def load_ora(procedure, run_mode, file, args, data): tempdir = tempfile.mkdtemp('gimp-plugin-file-openraster') orafile = zipfile.ZipFile(file.peek_path()) stack, w, h = get_image_attributes(orafile) Gimp.progress_init("Loading openraster image") img = Gimp.Image.new(w, h, Gimp.ImageBaseType.RGB) img.set_file(file) def get_layers(root): """iterates over layers and nested stacks""" for item in root: if item.tag == 'layer': yield item elif item.tag == 'stack': yield item for subitem in get_layers(item): yield subitem yield NESTED_STACK_END parent_groups = [] # Number of top level layers for tracking progress lay_cnt = len(stack) layer_no = 0 for item in get_layers(stack): prev_lay = layer_no if item is NESTED_STACK_END: parent_groups.pop() continue if item.tag == 'stack': name, x, y, opac, visible, layer_mode = get_group_layer_attributes( item) gimp_layer = Gimp.Layer.group_new(img) else: path, name, x, y, opac, visible, layer_mode = get_layer_attributes( item) if not path.lower().endswith('.png'): continue if not name: # use the filename without extension as name n = os.path.basename(path) name = os.path.splitext(n)[0] # create temp file. Needed because gimp cannot load files from inside a zip file tmp = os.path.join(tempdir, 'tmp.png') with open(tmp, 'wb') as fid: try: data = orafile.read(path) except KeyError: # support for bad zip files (saved by old versions of this plugin) data = orafile.read(path.encode('utf-8')) print( 'WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(path)) fid.write(data) # import layer, set attributes and add to image result = gimp_layer = Gimp.get_pdb().run_procedure( 'gimp-file-load-layer', [ GObject.Value(Gimp.RunMode, Gimp.RunMode.NONINTERACTIVE), GObject.Value(Gimp.Image, img), GObject.Value(Gio.File, Gio.File.new_for_path(tmp)), ]) if (result.index(0) == Gimp.PDBStatusType.SUCCESS): gimp_layer = gimp_layer.index(1) os.remove(tmp) else: print("Error loading layer from openraster image.") gimp_layer.set_name(name) gimp_layer.set_mode(layer_mode) gimp_layer.set_offsets(x, y) # move to correct position gimp_layer.set_opacity(opac * 100) # a float between 0 and 100 gimp_layer.set_visible(visible) img.insert_layer(gimp_layer, parent_groups[-1][0] if parent_groups else None, parent_groups[-1][1] if parent_groups else layer_no) if parent_groups: parent_groups[-1][1] += 1 else: layer_no += 1 if gimp_layer.is_group(): parent_groups.append([gimp_layer, 0]) if (layer_no > prev_lay): Gimp.progress_update(layer_no / lay_cnt) Gimp.progress_end() os.rmdir(tempdir) return Gimp.ValueArray.new_from_values([ GObject.Value(Gimp.PDBStatusType, Gimp.PDBStatusType.SUCCESS), GObject.Value(Gimp.Image, img), ])
def foggify(procedure, run_mode, image, n_drawables, drawables, args, data): config = procedure.create_config() config.begin_run(image, run_mode, args) if run_mode == Gimp.RunMode.INTERACTIVE: GimpUi.init('python-fu-foggify') dialog = GimpUi.ProcedureDialog.new(procedure, config) dialog.get_color_widget('color', True, GimpUi.ColorAreaType.FLAT) dialog.fill(None) if not dialog.run(): dialog.destroy() config.end_run(Gimp.PDBStatusType.CANCEL) return procedure.new_return_values(Gimp.PDBStatusType.CANCEL, GLib.Error()) else: dialog.destroy() color = config.get_property('color') name = config.get_property('name') turbulence = config.get_property('turbulence') opacity = config.get_property('opacity') Gimp.context_push() image.undo_group_start() if image.get_base_type() is Gimp.ImageBaseType.RGB: type = Gimp.ImageType.RGBA_IMAGE else: type = Gimp.ImageType.GRAYA_IMAGE for drawable in drawables: fog = Gimp.Layer.new(image, name, drawable.get_width(), drawable.get_height(), type, opacity, Gimp.LayerMode.NORMAL) fog.fill(Gimp.FillType.TRANSPARENT) image.insert_layer(fog, drawable.get_parent(), image.get_item_position(drawable)) Gimp.context_set_background(color) fog.edit_fill(Gimp.FillType.BACKGROUND) # create a layer mask for the new layer mask = fog.create_mask(0) fog.add_mask(mask) # add some clouds to the layer Gimp.get_pdb().run_procedure('plug-in-plasma', [ GObject.Value(Gimp.RunMode, Gimp.RunMode.NONINTERACTIVE), GObject.Value(Gimp.Image, image), GObject.Value(Gimp.Drawable, mask), GObject.Value(GObject.TYPE_INT, int(time.time())), GObject.Value(GObject.TYPE_DOUBLE, turbulence), ]) # apply the clouds to the layer fog.remove_mask(Gimp.MaskApplyMode.APPLY) fog.set_visible(True) Gimp.displays_flush() image.undo_group_end() Gimp.context_pop() config.end_run(Gimp.PDBStatusType.SUCCESS) return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
def command_for_procedure(self, proc_name): ''' Assemble string of Python code that when eval'd will call proc_name with contrived arguments. The purpose is to generate a template for a call to the PDB procedure the user has selected. The call MIGHT work as is. Otherwise, the names of the arguments might be enough that the user can figure out how to edit the template. The code will run in the environment of the console/browser, which is not the GIMP v2 GimpFu environment but the GIMP v3 PyGObject introspected environment. If ever GimpFu module is resurrected, and Python console imports it, then revert this code to its v2 form. ''' proc = Gimp.get_pdb().lookup_procedure(proc_name) if proc is None: return None cmd = '' return_values = proc.get_return_values() # assert is list of GParamSpec ''' Cat str of variable names to which unpack return values Variable names same as return value name, mangled Str like: 'retval_1, ret_val_2 = ' ''' if len(return_values) > 0: cmd += ', '.join( x.name.replace('-', '_') for x in return_values) cmd += ' = ' # else is a void PDB procedure ''' Cat prefix of str for a call to procedure name Prefix like: Gimp.get_pdb().run_procedure('<foo>', Note: - proc name is quoted, run_procedure wants a string. - proc name has hyphens. Not a Python name. Matches name in PDB. - trailing comma, another arg to follow: run_procedure takes two args: string name, and GValueArray of args ''' cmd += f"Gimp.get_pdb().run_procedure('{proc_name}', " ''' Assemble argument string. Using names of formal args, which might not match names already defined in the browsing environment (the REPL). Args are passed to a PDB procedure in a GValueArray. Assemble a string like '[arg_1, arg_2]'. When eval'd, the Python binding will convert to a GValueArray. ''' param_specs = proc.get_arguments() cmd += '[ ' ''' Special handling for run mode. GIMP v2: GimpFu had different handling for run mode. Insure run mode interactive, i.e. called procedure may open a GUI. This assumes that procedures use the same formal name for runmode arg. There might be rare other cases, especially for third party plugins? E.G. See formal signature of file-gex-load There is no other way to distinguish the run mode formal argument, as its formal type is GimpParamEnum, a generic enum. ''' if len(param_specs) > 0 and param_specs[0].name == 'run-mode': cmd += 'Gimp.RunMode.INTERACTIVE, ' param_specs = param_specs[1:] # else doesn't take a run mode arg # Cat string of arg names to a call # Like: 'arg_1, arg_2' where formal names arg-1 and arg-2 cmd += ', '.join(x.name.replace('-', '_') for x in param_specs) # terminate the arg array, and close parens the call cmd += '])' return cmd
def histogram_export(procedure, img, layers, gio_file, bucket_size, sample_average, output_format, progress_bar): layers = img.get_selected_layers() layer = layers[0] if sample_average: new_img = img.duplicate() layer = new_img.merge_visible_layers(Gimp.MergeType.CLIP_TO_IMAGE) channels_txt = ["Value"] channels_gimp = [Gimp.HistogramChannel.VALUE] if layer.is_rgb(): channels_txt += ["Red", "Green", "Blue", "Luminance"] channels_gimp += [ Gimp.HistogramChannel.RED, Gimp.HistogramChannel.GREEN, Gimp.HistogramChannel.BLUE, Gimp.HistogramChannel.LUMINANCE ] if layer.has_alpha(): channels_txt += ["Alpha"] channels_gimp += [Gimp.HistogramChannel.ALPHA] try: with open(gio_file.get_path(), "wt") as hfile: writer = csv.writer(hfile) # Write headers: writer.writerow(["Range start"] + channels_txt) max_index = 1.0 / bucket_size if bucket_size > 0 else 1 i = 0 progress_bar_int_percent = 0 while True: start_range = i * bucket_size i += 1 if start_range >= 1.0: break row = [start_range] for channel in channels_gimp: result = Gimp.get_pdb().run_procedure( 'gimp-drawable-histogram', [ GObject.Value(Gimp.Drawable, layer), GObject.Value(Gimp.HistogramChannel, channel), GObject.Value(GObject.TYPE_DOUBLE, float(start_range)), GObject.Value( GObject.TYPE_DOUBLE, float(min(start_range + bucket_size, 1.0))) ]) if output_format == output_format_enum.pixel_count: count = int(result.index(5)) else: pixels = result.index(4) count = (result.index(5) / pixels) if pixels else 0 if output_format == output_format_enum.percent: count = "%.2f%%" % (count * 100) row.append(str(count)) writer.writerow(row) # Update progress bar if progress_bar: fraction = i / max_index # Only update the progress bar if it changed at least 1% . new_percent = math.floor(fraction * 100) if new_percent != progress_bar_int_percent: progress_bar_int_percent = new_percent progress_bar.set_fraction(fraction) # Make sure the progress bar gets drawn on screen. while Gtk.events_pending(): Gtk.main_iteration() except IsADirectoryError: return procedure.new_return_values( Gimp.PDBStatusType.EXECUTION_ERROR, GLib.Error(_("File is either a directory or file name is empty."))) except FileNotFoundError: return procedure.new_return_values( Gimp.PDBStatusType.EXECUTION_ERROR, GLib.Error(_("Directory not found."))) except PermissionError: return procedure.new_return_values( Gimp.PDBStatusType.EXECUTION_ERROR, GLib.Error("You do not have permissions to write that file.")) if sample_average: new_img.delete() return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
def load_ora(procedure, run_mode, file, args, data): tempdir = tempfile.mkdtemp('gimp-plugin-file-openraster') orafile = zipfile.ZipFile(file.peek_path()) stack, w, h = get_image_attributes(orafile) img = Gimp.Image.new(w, h, Gimp.ImageBaseType.RGB) img.set_filename(file.peek_path()) def get_layers(root): """iterates over layers and nested stacks""" for item in root: if item.tag == 'layer': yield item elif item.tag == 'stack': yield item for subitem in get_layers(item): yield subitem yield NESTED_STACK_END parent_groups = [] layer_no = 0 for item in get_layers(stack): if item is NESTED_STACK_END: parent_groups.pop() continue if item.tag == 'stack': name, x, y, opac, visible, layer_mode = get_group_layer_attributes( item) gimp_layer = img.layer_group_new() else: path, name, x, y, opac, visible, layer_mode = get_layer_attributes( item) if not path.lower().endswith('.png'): continue if not name: # use the filename without extension as name n = os.path.basename(path) name = os.path.splitext(n)[0] # create temp file. Needed because gimp cannot load files from inside a zip file tmp = os.path.join(tempdir, 'tmp.png') with open(tmp, 'wb') as fid: try: data = orafile.read(path) except KeyError: # support for bad zip files (saved by old versions of this plugin) data = orafile.read(path.encode('utf-8')) print( 'WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(path)) fid.write(data) # import layer, set attributes and add to image args = Gimp.ValueArray.new(3) arg0 = GObject.Value(Gimp.RunMode, Gimp.RunMode.NONINTERACTIVE) args.insert(0, arg0) arg1 = GObject.Value(Gimp.Image, img) args.insert(1, arg1) arg2 = GObject.Value(GObject.TYPE_STRING, tmp) args.insert(2, arg2) gimp_layer = Gimp.get_pdb().run_procedure('gimp-file-load-layer', args) gimp_layer = gimp_layer.index(1) gimp_layer = Gimp.Item.get_by_id(gimp_layer) os.remove(tmp) gimp_layer.set_name(name) gimp_layer.set_mode(layer_mode) gimp_layer.set_offsets(x, y) # move to correct position gimp_layer.set_opacity(opac * 100) # a float between 0 and 100 gimp_layer.set_visible(visible) img.insert_layer(gimp_layer, parent_groups[-1][0] if parent_groups else None, parent_groups[-1][1] if parent_groups else layer_no) if parent_groups: parent_groups[-1][1] += 1 else: layer_no += 1 if gimp_layer.is_group(): parent_groups.append([gimp_layer, 0]) os.rmdir(tempdir) retval = Gimp.ValueArray.new(2) arg0 = GObject.Value(Gimp.PDBStatusType, Gimp.PDBStatusType.SUCCESS) retval.insert(0, arg0) arg1 = GObject.Value(Gimp.Image, img) retval.insert(1, arg1) return retval
def save_ora(procedure, run_mode, image, drawable, file, args, data): def write_file_str(zfile, fname, data): # work around a permission bug in the zipfile library: # http://bugs.python.org/issue3394 zi = zipfile.ZipInfo(fname) zi.external_attr = int("100644", 8) << 16 zfile.writestr(zi, data) tempdir = tempfile.mkdtemp('gimp-plugin-file-openraster') # use .tmpsave extension, so we don't overwrite a valid file if # there is an exception orafile = zipfile.ZipFile(file.peek_path() + '.tmpsave', 'w', compression=zipfile.ZIP_STORED) write_file_str(orafile, 'mimetype', 'image/openraster') # must be the first file written # build image attributes xml_image = ET.Element('image') stack = ET.SubElement(xml_image, 'stack') a = xml_image.attrib a['w'] = str(image.width()) a['h'] = str(image.height()) def store_layer(image, drawable, path): tmp = os.path.join(tempdir, 'tmp.png') interlace, compression = 0, 2 args = Gimp.ValueArray.new(11) args.insert(0, GObject.Value(Gimp.RunMode, Gimp.RunMode.NONINTERACTIVE)) args.insert(1, GObject.Value(Gimp.Image, image)) args.insert(2, GObject.Value(Gimp.Drawable, drawable)) args.insert(3, GObject.Value(GObject.TYPE_STRING, tmp)) args.insert(4, GObject.Value(GObject.TYPE_STRING, 'tmp.png')) args.insert(5, GObject.Value(GObject.TYPE_BOOLEAN, interlace)) args.insert(6, GObject.Value(GObject.TYPE_INT, compression)) # write all PNG chunks except oFFs(ets) args.insert(7, GObject.Value(GObject.TYPE_BOOLEAN, True)) args.insert(8, GObject.Value(GObject.TYPE_BOOLEAN, True)) args.insert(9, GObject.Value(GObject.TYPE_BOOLEAN, False)) args.insert(10, GObject.Value(GObject.TYPE_BOOLEAN, True)) Gimp.get_pdb().run_procedure('file-png-save', args) orafile.write(tmp, path) os.remove(tmp) def add_layer(parent, x, y, opac, gimp_layer, path, visible=True): store_layer(image, gimp_layer, path) # create layer attributes layer = ET.Element('layer') parent.append(layer) a = layer.attrib a['src'] = path a['name'] = gimp_layer.get_name() a['x'] = str(x) a['y'] = str(y) a['opacity'] = str(opac) a['visibility'] = 'visible' if visible else 'hidden' a['composite-op'] = reverse_map(layermodes_map).get( gimp_layer.get_mode(), 'svg:src-over') return layer def add_group_layer(parent, opac, gimp_layer, visible=True): # create layer attributes group_layer = ET.Element('stack') parent.append(group_layer) a = group_layer.attrib a['name'] = gimp_layer.name a['opacity'] = str(opac) a['visibility'] = 'visible' if visible else 'hidden' a['composite-op'] = reverse_map(layermodes_map).get( gimp_layer.get_mode(), 'svg:src-over') return group_layer def enumerate_layers(layers): for layer in layers: if not layer.is_group(): yield layer else: yield layer for sublayer in enumerate_layers(layer.get_children()): yield sublayer yield NESTED_STACK_END # save layers parent_groups = [] i = 0 for lay in enumerate_layers(image.get_layers()): if lay is NESTED_STACK_END: parent_groups.pop() continue _, x, y = lay.offsets() opac = lay.get_opacity() / 100.0 # needs to be between 0.0 and 1.0 if not parent_groups: path_name = 'data/{:03d}.png'.format(i) i += 1 else: path_name = 'data/{}-{:03d}.png'.format(parent_groups[-1][1], parent_groups[-1][2]) parent_groups[-1][2] += 1 parent = stack if not parent_groups else parent_groups[-1][0] if lay.is_group(): group = add_group_layer(parent, opac, lay, lay.get_visible()) group_path = ("{:03d}".format(i) if not parent_groups else parent_groups[-1][1] + "-{:03d}".format(parent_groups[-1][2])) parent_groups.append([group, group_path, 0]) else: add_layer(parent, x, y, opac, lay, path_name, lay.get_visible()) # save mergedimage args = Gimp.ValueArray.new(1) args.insert(0, GObject.Value(Gimp.Image, image)) thumb = Gimp.get_pdb().run_procedure('gimp-image-duplicate', args) thumb = thumb.index(1) thumb = Gimp.Image.get_by_id(thumb) thumb_layer = thumb.merge_visible_layers(Gimp.MergeType.CLIP_TO_IMAGE) store_layer(thumb, thumb_layer, 'mergedimage.png') # save thumbnail w, h = image.width(), image.height() if max(w, h) > 256: # should be at most 256x256, without changing aspect ratio if w > h: w, h = 256, max(h * 256 / w, 1) else: w, h = max(w * 256 / h, 1), 256 thumb_layer.scale(w, h, False) if thumb.get_precision() != Gimp.Precision.U8_GAMMA: thumb.convert_precision(Gimp.Precision.U8_GAMMA) store_layer(thumb, thumb_layer, 'Thumbnails/thumbnail.png') thumb.delete() # write stack.xml xml = ET.tostring(xml_image, encoding='UTF-8') write_file_str(orafile, 'stack.xml', xml) # finish up orafile.close() os.rmdir(tempdir) if os.path.exists(file.peek_path()): os.remove(file.peek_path()) # win32 needs that os.rename(file.peek_path() + '.tmpsave', file.peek_path()) retval = Gimp.ValueArray.new(1) retval.insert( 0, GObject.Value(Gimp.PDBStatusType, Gimp.PDBStatusType.SUCCESS)) return retval
def _adaptor_func(self, *args, **kwargs): """ Run a PDB procedure whose name was used like "pdb.name()" e.g. like a method call of pdb object. Crux: wrap a call to PDB.run_procedure() Wrapping requires marshalling args from Python types to GObject types. Wrapping also requires inserting run_mode arg (GimpFu hides that from Authors.) Args are from Author. That is, they are external (like i/o, beyond our control). Thus we catch exceptions (and check for other errors) and proceed. """ self.logger.debug(f"_adaptor_func called, args: {args}") if kwargs: proceed(f"PDB procedures do not take keyword args.") # !!! avoid infinite recursion proc_name = object.__getattribute__(self, "adapted_proc_name") # !!! Must unpack args before passing to _marshall_args try: marshaled_args = MarshalPDB.marshal_args(proc_name, *args) except Exception as err: # TODO catch only MarshalError ??? proceed(f"marshalling args to pdb.{proc_name} {err}") marshaled_args = None if marshaled_args is not None: # marshaled_args is-a list of GValues, but it could be an empty list. # PyGObject will marshall the list into a GimpValueArray """ This is almost always a segfaulted callee plugin, a separate process that crashed and is failing to respond to IPC. We assert and don't try/except/proceed because the error is serious and external to Author's plugin. """ inner_result = Gimp.get_pdb().run_procedure( proc_name, marshaled_args) assert inner_result is not None, f"PDB procedure {proc_name} failed to return value array." # The first element of result is the PDB status self.logger.debug( f"run_procedure {proc_name}, result is: {inner_result.index(0)}" ) # pdb is stateful for errors, i.e. gets error from last invoke, and resets on next invoke error_str = Gimp.get_pdb().get_last_error() if error_str != 'success': # ??? GIMP_PDB_SUCCESS """ Log the args because it is a common failure: wrong args. We might also log what Author gave (args) before we marshalled them. TODO i.e. { {*args} } ?, but that leaves braces in the output TODO { {*args} } throws "unhashable type GimpfuImage" """ self.logger.warning(f"Args: {marshaled_args}") proceed(f"PDB call fail: {proc_name} Gimp says: {error_str}") result = None else: result = MarshalPDB.unmarshal_results(inner_result) else: result = None # This is the simplified view of what we just did, without all the error checks # object.__getattribute__(self, "_marshall_args")(proc_name, *args) # Most PDB calls have side_effects on image, but few return values? # ensure result is defined and (is-a list OR None) # TODO throws for GBoxed, so log the types and not the values self.logger.debug(f"_adaptor_func for: {proc_name} returns: {result}") return result
def run(self, procedure, run_mode, image, n_drawables, drawables, args, run_data): if n_drawables != 1: msg = _( f"Procedure '{procedure.get_name()}' only works with one drawable." ) error = GLib.Error.new_literal(Gimp.PlugIn.error_quark(), msg, 0) return procedure.new_return_values( Gimp.PDBStatusType.CALLING_ERROR, error) else: drawable = drawables[0] # check if selection exist selection = image.get_selection() flag, non_empty, x1, y1, x2, y2 = selection.bounds(image) if not non_empty: msg = _( f"The selection is empty, create a selection box and precede with the use of this plugin." ) error = GLib.Error.new_literal(Gimp.PlugIn.error_quark(), msg, 0) return procedure.new_return_values( Gimp.PDBStatusType.CALLING_ERROR, error) if run_mode == Gimp.RunMode.INTERACTIVE: gi.require_version('Gtk', '3.0') from gi.repository import Gtk gi.require_version('Gdk', '3.0') from gi.repository import Gdk GimpUi.init("add_balloon.py") dialog = GimpUi.Dialog(use_header_bar=True, title=_("Add Balloon)"), role="add_balloon-Python3") dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL) dialog.add_button(_("_OK"), Gtk.ResponseType.OK) geometry = Gdk.Geometry() geometry.min_aspect = 0.5 geometry.max_aspect = 1.0 dialog.set_geometry_hints(None, geometry, Gdk.WindowHints.ASPECT) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) dialog.get_content_area().add(box) box.show() # Label text content label = Gtk.Label(label='Text:') box.pack_start(label, False, False, 1) label.show() # scroll area for text scrolled = Gtk.ScrolledWindow() scrolled.set_vexpand(True) box.pack_start(scrolled, True, True, 1) scrolled.show() # text content box text_content = Gtk.TextView() contents = 'text' buffer = text_content.get_buffer() buffer.set_text(contents, -1) scrolled.add(text_content) text_content.show() # Improve UI font_chooser = Gtk.FontChooserWidget() box.pack_start(font_chooser, False, False, 1) font_chooser.show() # TODO add spinner for waiting while (True): response = dialog.run() if response == Gtk.ResponseType.OK: # TODO enable spinner and lock all other values # layer position position = Gimp.get_pdb().run_procedure( 'gimp-image-get-item-position', [image, drawable]).index(1) # Create group layer_group = Gimp.get_pdb().run_procedure( 'gimp-layer-group-new', [image]).index(1) image.insert_layer(layer_group, None, position) # add new trasparent layer overlay_layer = Gimp.Layer.new(image, 'hide_background', drawable.get_width(), drawable.get_height(), Gimp.ImageType.RGBA_IMAGE, 100.0, Gimp.LayerMode.NORMAL) image.insert_layer(overlay_layer, layer_group, position) overlay_layer.fill(Gimp.FillType.TRANSPARENT) # add white fill the selection Gimp.get_pdb().run_procedure( 'gimp-drawable-edit-fill', [overlay_layer, Gimp.FillType.WHITE]) # add text layer buffer = text_content.get_buffer() text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True) font_str = font_chooser.get_font() font_size = float(font_str.split(' ')[-1]) font_name = ' '.join(font_str.split(' ')[0:-2]) text_layer = Gimp.get_pdb().run_procedure( 'gimp-text-layer-new', [image, text, font_name, font_size, 3]).index(1) image.insert_layer(text_layer, layer_group, position - 1) # center text layer Gimp.get_pdb().run_procedure( 'gimp-text-layer-set-justification', [text_layer, 2]) cx = (x1 + x2) / 2 - text_layer.get_width() / 2 cy = (y1 + y2) / 2 - text_layer.get_height() / 2 Gimp.get_pdb().run_procedure( 'gimp-item-transform-translate', [text_layer, cx, cy]) # set selected layer image.set_selected_layers([layer_group]) dialog.destroy() break else: dialog.destroy() return procedure.new_return_values( Gimp.PDBStatusType.CANCEL, GLib.Error()) return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())