#!/usr/bin/env python

"""
ShellOut.py
call an external program passing the active layer as a temp file.  Windows Only(?)

Author:
Rob Antonishen

Version:
0.8 updated for GIMP 3.x compatibility
0.7 fixed file save bug where all files were png regardless of extension
0.6 modified to allow for a returned layer that is a different size
   than the saved layer for
0.5 file extension parameter in program list.
0.4 modified to support many optional programs.

this script is modelled after the mm extern LabCurves trace plugin
by Michael Munzert http://www.mm-log.com/lab-curves-gimp

and thanks to the folds at gimp-chat has grown a bit ;)

License:

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 3 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

The GNU Public License is available at
http://www.gnu.org/copyleft/gpl.html

"""

import gi
gi.require_version('Gimp', '3.0')

import shlex
import subprocess
import os, sys
import tempfile

from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gimp

# Define plug-in metadata
PROC_NAME = "python-fu-shellout"
HELP = "Call an external program"
DOC = "Call an external program passing the active layer as a temp file"
AUTHOR = "Rob Antonishen"
COPYRIGHT = "Copyright 2011 Rob Antonishen"
DATE = "2025-03-24"

# Program list function (globals are evil)
def listcommands(option=None):

    # Insert additional shell command into this list. They will show up in the drop menu in this order.
    # Use the syntax:
    # ["Menu Label", "command", "ext"]
    #
    # Where what gets executed is command filename, so include any flags needed in the command.
    programlist = [
        ["DFine 2", "\"C:\\Program Files\\Google\\Nik Collection\\Dfine 2\\Dfine2.exe\"", "png"],
        ["Sharpener Pro 3", "\"C:\\Program Files\\Google\\Nik Collection\\Sharpener Pro 3\\SHP3OS.exe\"", "png"],
        ["Viveza 2", "\"C:\\Program Files\\Google\\Nik Collection\\Viveza 2\\Viveza 2.exe\"", "png"],
        ["Color Efex Pro 4", "\"C:\\Program Files\\Google\\Nik Collection\\Color Efex Pro 4\\Color Efex Pro 4.exe\"",
         "jpg"],
        ["Analog Efex Pro 2", "\"C:\\Program Files\\Google\\Nik Collection\\Analog Efex Pro 2\\Analog Efex Pro 2.exe\"",
         "jpg"],
        ["HDR Efex Pro 2", "\"C:\\Program Files\\Google\\Nik Collection\\HDR Efex Pro 2\\HDR Efex Pro 2.exe\"", "jpg"],
        ["Silver Efex Pro 2", "\"C:\\Program Files\\Google\\Nik Collection\\Silver Efex Pro 2\\Silver Efex Pro 2.exe\"",
         "jpg"],
        ["", "", ""]
    ]

    if option is None:  # no parameter return menu list, otherwise return the appropriate array
        menulist = []
        for i in programlist:
            if i[0] != "":
                menulist.append(i[0])
        return menulist
    else:
        return programlist[option]


def plugin_main(procedure, run_mode, image, n_drawables, drawables, config, data):

    # Get parameters
    visible = config.get_property("visible")
    command_idx = config.get_property("command")

    if n_drawables == 0:
        return procedure.new_return_values(Gimp.PDBStatusType.CALLING_ERROR, GLib.Error())

    drawable = drawables[0]

    # Start an undo group
    Gimp.context_push()
    image.undo_group_start()

    # Copy so the save operations doesn't affect the original
    if visible == 0:
        # Use the active drawable
        temp = drawable
    else:
        # Get the current visible
        temp = Gimp.Layer.new_from_visible(image, image, "Visible")
        image.insert_layer(temp, None, 0)

    # Copy the layer content
    buffer = Gimp.edit_named_copy([temp], "ShellOutTemp")

    # Save selection if one exists
    hassel = not Gimp.Selection.is_empty(image)
    if hassel:
        savedsel = Gimp.Selection.save(image)

    # Create a new image with the copied content
    tempimage = Gimp.edit_named_paste_as_new_image(buffer)
    Gimp.Buffer.delete(buffer)
    if not tempimage:
        image.undo_group_end()
        Gimp.context_pop()
        return procedure.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR, GLib.Error())

    Gimp.Image.undo_disable(tempimage)

    # Get the active layer from the temp image
    tempdrawable = Gimp.Image.get_active_layer(tempimage)

    # Get the program to run and filetype
    progtorun = listcommands(command_idx)

    # Use temp file names from gimp, it reflects the user's choices in gimp.rc
    # change as indicated if you always want to use the same temp file name
    # tempfilename = pdb.gimp_temp_name(progtorun[2])
    tempfilename = os.path.join(tempfile.gettempdir(), "ShellOutTempFile." + progtorun[2])

    # !!! Note no run-mode first parameter, and user entered filename is empty string
    Gimp.progress_init("Saving a copy")
    Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, tempimage, tempdrawable, GLib.file_new_for_path(tempfilename))

    # Build command line call
    command = progtorun[1] + " \"" + tempfilename + "\""
    args = shlex.split(command)

    # Invoke external command
    Gimp.progress_init("Calling " + progtorun[0] + "...")
    Gimp.progress_pulse()
    child = subprocess.Popen(args, shell=False)
    child.communicate()

    # put it as a new layer in the opened image
    try:
        newlayer2 = Gimp.file_load_layer(Gimp.RunMode.NONINTERACTIVE, tempimage, GLib.file_new_for_path(tempfilename))
    except Exception as e:
        print(f"Error loading file: {e}")
        image.undo_group_end()
        Gimp.context_pop()
        return procedure.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR, GLib.Error())
    
    tempimage.insert_layer(newlayer2, None, -1)
    buffer = Gimp.edit_named_copy([newlayer2], "ShellOutTemp")

    if visible == 0:
        Gimp.Item.resize(drawable, newlayer2.get_width(), newlayer2.get_height(), 0, 0)
        sel = Gimp.edit_named_paste(drawable, buffer, True)
        Gimp.Item.transform_translate(drawable, 
                                     (tempdrawable.get_width() - newlayer2.get_width()) / 2,
                                     (tempdrawable.get_height() - newlayer2.get_height()) / 2)
    else:
        Gimp.Item.resize(temp, newlayer2.get_width(), newlayer2.get_height(), 0, 0)
        sel = Gimp.edit_named_paste(temp, buffer, True)
        Gimp.Item.transform_translate(temp, 
                                    (tempdrawable.get_width() - newlayer2.get_width()) / 2,
                                    (tempdrawable.get_height() - newlayer2.get_height()) / 2)

    Gimp.Buffer.delete(buffer)
    Gimp.edit_clear([temp])
    Gimp.floating_sel_anchor(sel)

    # load up old selection
    if hassel:
        Gimp.Selection.load(savedsel)
        image.remove_channel(savedsel)

    # cleanup
    os.remove(tempfilename)  # delete the temporary file
    tempimage.delete()       # delete the temporary image

    # End the undo group
    image.undo_group_end()
    Gimp.displays_flush()
    Gimp.context_pop()
    
    return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())


class ShellOut(Gimp.PlugIn):

    def do_query_procedures(self):
        """Return the name of the procedure this plugin defines"""
        return [PROC_NAME]

    def do_create_procedure(self, name):
        """Create the procedure"""
        procedure = Gimp.ImageProcedure.new(self, name,
                                           Gimp.PDBProcType.PLUGIN,
                                           plugin_main, None)
        
        procedure.set_image_types("RGB*, GRAY*")
        procedure.set_menu_label("ShellOut...")
        procedure.set_attribution(AUTHOR, COPYRIGHT, DATE)
        procedure.set_documentation(HELP, DOC, None)

        procedure.add_menu_path("<Image>/Filters/ShellOut...")

        # FIXME: How to open menu and get return values and pass into main_plugin()?
        
        return procedure

Gimp.main(ShellOut.__gtype__, sys.argv)