Example #1
0
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),
    ])
Example #2
0
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())
Example #3
0
        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
Example #4
0
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())
Example #5
0
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
Example #6
0
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
Example #7
0
    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
Example #8
0
    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())