Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
"Simple" python script in gimp 3
#1
Hello!
For a game I'd like to create tiles for tileset. The task is simple: get a image with 3+ layers (colors, and heightmap), for each layer create 3 copy, rotate it 60, 180 ,270 degree and offset it 1*,2*,3* width.
After get the heightmap layer duplicate it, add noise and call the normal map filter. (there are some masking, etc). Get the color layers, add some nose. Flattering down each group of layers.
Because I'd like to use it many times, i created (or try to create) a plugin for it.
Looks like easy.

The problems: because its a python, you need to test is, for the typos too. so each iteration need to restart gimp load the image test the script.
There are no usable documentation. I already found the https://lazka.github.io/pgi-docs/#Gimp-3.0, helped a lot, but lack of examples. all examples for the gimp 2.x
For example, how can I do filter->generic->normal map?
There are some undocumented "features": If you rename a layer, in the gimp UI the name stay the old one (its not a big problem) but after you cant export it into .ora and gimp went some unstable state. If remove a layer from image cant add it again to it, because already had.

Now I have 300+ line of code for that "simple" task, about quarter are dead code ("maybe that work") and not working well:

- after I call <layer>.transform_rotate_simple(rotate_angle,True,0,0) its stay is "floating selection transform" state and somehow need to anchor it how?
- how can I proper call the filters->generic->normal map filter with preset or how set the values?
- how can I proper call the filters->noise->hsv filter with preset or how set the values?
- how can I invert selection?
- how can I proper generate greyscale noise between color rgb(123,123,123) and rgb  (133,133,133), I tried the buffer.set(rect, "RGBA u8", bytes(pixels)) with pixels.extend([gray, gray, gray, 255]) it's worked, but not get the right color (I get lighter about  rgb(180,180,180), now I use set_pixel but its slow for 64x64 images
- from where can I get some useful examples for gimp 3 python scripting?

Thanx
Reply
#2
IgnisVeneficus: Hi! You don't need to restart GIMP each time you edit your Python script. As long as it loads the first time successfully, you can edit the script and rerun it, and it should run the updated the version.
(The only exception is if you add new parameters to the procedure)

You can anchor a floating selection with Gimp.floating_sel_anchor (): Gimp.floating_sel_anchor

Invert a section with Gimp.Selection.invert(): Gimp.Selection.invert

You can use the DrawableFilter API to add filters like Normal Mapp and HSV: Gimp.DrawableFilter.new

Normal Map parameters: https://gegl.org/operations/gegl-normal-map.html
HSV Noise parameters: https://gegl.org/operations/gegl-noise-hsv.html
Reply
#3
CmykStudent: Hi!
Thanx!
All is working. Now the script do what I wanted!
Just one little problem. After the script finished, I cant save/export the image, because all the menu items (save, save as, export, export as) as grayed out, and the hot keys don't work too. But I tried to close it, i get the "image is not saved" window. but i cant save with that too..
Thanx
Reply
#4
(07-29-2025, 07:10 PM)IgnisVeneficus Wrote: CmykStudent: Hi!
Thanx!
All is working. Now the script do what I wanted!
Just one little problem. After the script finished, I cant save/export the image, because all the menu items (save, save as, export, export as) as grayed out, and the hot keys don't work too. But I tried to close it, i get the "image is not saved" window. but i cant save with that too..
Thanx

Hmm. Can you share the script? I can take a look and see if there's some issue.
Reply
#5
Hi!
Sure:
Code:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import os
import gi
import random
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
from gi.repository import Gio


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

class TilesetVariants (Gimp.PlugIn):
   def do_query_procedures(self):
       return [ "plug-in-tileset-variants" ]

   def do_create_procedure(self, name):
       procedure = Gimp.ImageProcedure.new(self, name,
                                           Gimp.PDBProcType.PLUGIN,
                                           self.run, None)

       procedure.set_image_types("*")
       procedure.set_sensitivity_mask (Gimp.ProcedureSensitivityMask.DRAWABLE)

       procedure.set_menu_label(_("tileset"))
       procedure.add_menu_path('<Image>/Filters/Tileset/')

       procedure.set_documentation(_("Plug-in example in Python 3"),
                                   _("Plug-in example in Python 3"),
                                   name)
       procedure.set_attribution("ignis", "ignis", "2025")

       return procedure

   def run(self, procedure, run_mode, image, drawables, config, run_data):
       Gimp.message("running")
       try:
           tileset_variants(image,drawables)
       except Exception as e:
           Gimp.message(f"error: {e}")

       # do what you want to do, then, in case of success, return:
       return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())


#apply normal filter to the layer
def apply_normalMap(layer):
   filter = Gimp.DrawableFilter.new(layer, "gegl:normal-map", "Normal Map")
   filter.set_blend_mode(Gimp.LayerMode.REPLACE)
   filter.set_opacity(1.0)
   config = filter.get_config()
   config.set_property('scale', 100)
   filter.update()
   layer.append_filter(filter)    

#apply hsv noise to the filter
def apply_noise(layer):
   filter = Gimp.DrawableFilter.new(layer, "gegl:noise-hsv", "Add HSV Noise")
   filter.set_blend_mode(Gimp.LayerMode.REPLACE)
   filter.set_opacity(1.0)
   #config = filter.get_config()
   #config.set_property('Dulling', 2)
   #config.set_property('Hue', 3)
   #config.set_property('Saturation', 0.04)
   #config.set_property('Value', 0.04)

   filter.update()
   layer.append_filter(filter)    

#create a layer, and fill up with grayscale noise between min and max value
def generate_noise(image,min,max,w,h):
   layers = []
   for i in range(4):
       layer = Gimp.Layer.new(image, "Noise_" + str(i), w, h,
                      Gimp.ImageType.RGBA_IMAGE, 100.0, Gimp.LayerMode.NORMAL)
       image.insert_layer(layer, None, -1)

       color_format = "#{:02x}{:02x}{:02x}"
       colors = []
       for c in range(min,max+1):
           colors.append(Gegl.Color.new(color_format.format(c,c,c)))
               
       for y in range(h):
           for x in range(w):
               cp = random.randint(0,len(colors)-1)
               #pixels.extend([gray, gray, gray, 255])  # R, G, B, A
               layer.set_pixel(x,y,colors[cp])
     
       layer.merge_shadow(True)
       layer.update(0, 0, w, h)

       layer.set_offsets(i * w, 0)
       layer.set_mode(Gimp.LayerMode.OVERLAY)
       layer.set_visible(False)

       layers.append(layer)
   return layers

#rename the layer: workaround: remove layer, copy layer, rename new layer, add to image the new layer
def rename_layer(image,layer,name):
   if not layer:
       return None
   parent = layer.get_parent()
   position = image.get_item_position(layer)
   image.remove_layer(layer)
   new_layer = layer.copy()
   new_layer.set_name(name)
   image.insert_layer(new_layer, parent, position)
   return new_layer

#create rotated images and offset is
def generate_variants(image,layer,width):
   if layer == None:
       return [None]*4
   variants = [layer]
   for angle in range(1,4):
       rotated = layer.copy()
       rotated.set_name(layer.get_name()+"_"+str(angle))
       image.insert_layer(rotated, None, -1)
       rotate_angle = Gimp.RotationType.DEGREES90
       if angle == 2:
           rotate_angle =Gimp.RotationType.DEGREES180
       if angle == 3:
           rotate_angle =Gimp.RotationType.DEGREES270
       rotated.transform_rotate_simple(rotate_angle,True,0,0)
       floating=image.get_selected_layers()
       # many times get an error, cant anchor not floating image
       # if I leave it, one or two image not moved and/or not rotated
       try:
           Gimp.floating_sel_anchor(floating[0])
       except Exception as e:
           pass
       rotated.set_offsets(angle * width, 0)
       variants.append(rotated)

   return variants

# get the mask layer, select the black color, and fill the selection with neutral gray on the noise layer
def mask_noise(componed_layers,image):
   Gimp.context_set_background(Gegl.Color.new("#808080"))

   for i in range(4):
       mask = componed_layers["Mask"][i]
       if mask!=None:
           image.select_color(Gimp.ChannelOps.REPLACE, mask, Gegl.Color.new("#000000"))
           noise_layer = componed_layers["Noise"][i]
           image.active_layer =  noise_layer
           noise_layer.edit_fill(Gimp.FillType.BACKGROUND)
           # to be sure noting is selected
           selection = image.get_selection()
           selection.none(image)

# get the noise layer, the normal layer, and the noise addon, and merged together
def merge_noise(componed_layers,image):
   #merge noise es tarsai:
   for i in range(4):
       noise = componed_layers["Noise"][i]
       normal = componed_layers["Normal"][i]
       addon = componed_layers["Addon"][i]
       noise.set_visible(True)
       normal.set_visible(True)
       image.reorder_item(normal,None,-1)
       image.reorder_item(noise, None,-1)
       name = normal.get_name()
       Gimp.message(normal.get_name())
       if addon!=None:
           addon.set_visible(True)
           image.reorder_item(addon, None,-1)
       image.active_layer =  normal
       normal = image.merge_visible_layers(Gimp.MergeType.EXPAND_AS_NECESSARY)
       #need to rename the last layer
       Gimp.message(normal.get_name())
       normal=rename_layer(image,normal,name)
       componed_layers["Normal"][i]=normal
       normal.set_visible(False)

# generate noise for the Color image
# get the mask, select the black, invert the selection, and generate the noise into the selection on the color layer
def color_noise(componed_layers,image):

   for i in range(4):
       color = componed_layers["Color"][i]
       mask = componed_layers["ColorMask"][i]
       color.set_visible(True)
       image.reorder_item(color,None,-1)
       apply = color
       if mask!=None:
           mask.set_visible(True)
           image.reorder_item(mask, None,-1)
           image.select_color(Gimp.ChannelOps.REPLACE, mask, Gegl.Color.new("#000000"))
           selection = image.get_selection()
           selection.invert(image)
           apply = selection
       image.active_layer =  color
       
       apply_noise(apply)
       selection = image.get_selection()
       if selection:
           selection.none(image)
       color.set_visible(False)

# main function
def tileset_variants(image, drawable):
   Gimp.message("variant running")
   # get the file name to the export
   orig_path = image.get_file().get_path()
   basename = os.path.basename(orig_path)
   dirname = os.path.dirname(orig_path)
   new_name = os.path.splitext(basename)[0] + ".ora"
   output_path = os.path.join(dirname, new_name)
   Gimp.message("before")

   image.undo_group_start()

   #set up dedicated layers
   color = None
   heightmap = None
   mask = None
   normal = None
   addon = None
   colormask = None

   #Extract layers
   for layer in image.get_layers():
       layer.set_visible(False)
       # Color: visible texture
       if layer.get_name() == "Color":
           color = layer
       # Heightmap    
       if layer.get_name() == "Heightmap":
           heightmap = layer
       # Mask, where dont need noise for heightmap
       if layer.get_name() == "Noisemask":    
           mask = layer
       # layer added to the heightmap aka details
       if layer.get_name() == "Heightaddon":    
           addon = layer
       # color mask, where deont need noise for color layer
       if layer.get_name() == "Colormask":    
           colormask = layer


   width = image.get_width()
   height = image.get_height()

   # resize image
   image.resize(width * 4, height,0,0)

   # duplicate heightmap, rename to normal
   normal = heightmap.copy()
   normal.set_name("Normal")
   image.insert_layer(normal, None, -1)

   #collect the layers
   all_layers = {"Color":color, "Heightmap":heightmap, "Normal":normal,"Mask":mask,"Addon":addon,"ColorMask":colormask}

   # rotate and shift, create dict for layers
   componed_layers={}
   for name,layer in all_layers.items():
       variants = generate_variants(image,layer,width)
       componed_layers[name]=variants
   
   # generate noise layer
   componed_layers["Noise"]=generate_noise(image,123,133,width,height)

   # create noise
   mask_noise(componed_layers,image)

   #create heightmap for normal (apply noise, mask, details onto the copied heightmap)
   merge_noise(componed_layers,image)

   #apply normal map filter
   for i in range(4):
       normal = componed_layers["Normal"][i]
       apply_normalMap(normal)

   #apply color noise
   color_noise(componed_layers,image)

   #merge down each group
   Gimp.message("flattening")
   for name,layer in componed_layers.items():
       Gimp.message("group: "+name)
       needMerge = False
       for l in layer:
           if l:
               Gimp.message("layer: "+l.get_name())
               l.set_visible(True)
               image.active_layer =  l
               needMerge = True
       if needMerge:
           merged = image.merge_visible_layers(Gimp.MergeType.EXPAND_AS_NECESSARY)
           if merged:
               Gimp.message("rename: "+merged.get_name())
               merged=rename_layer(image,merged,name)      
               merged.set_visible(False)
               image.active_layer =  merged

   #try to export as openraster
   Gimp.file_save(Gimp.RunMode.INTERACTIVE,image, Gio.File.new_for_path(output_path),None)
   
   image.undo_group_end()    
   Gimp.message("End")


Gimp.main(TilesetVariants.__gtype__, sys.argv)
I tried to put a lots of comment

Thanx
Csaba
Reply
#6
IgnisVeneficus: Hi again! I haven't been able to replicate the problem so far - it lets me export or save the final result. I did notice one thing - by default, GIMP does not create floating selections anymore when you copy and paste. Instead, it creates a new layer. That's why Gimp.floating_sel_anchor () throws errors. You'll either need to paste as a floating layer or merge the layer down instead.
Reply
#7
Hello!
About the floating selection.
If I comment out the whole anchore thing, I noticed, during the duplicate-rotate-move there is a floating layer, and the first layer, first target is missing/or not moved/rotated
(see attachement)

Thanx
Ignis


Attached Files Thumbnail(s)
   
Reply


Forum Jump: