Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Converting python plugin-in shellout.py from gimp 2.x to 3.x
#21
(03-26-2025, 10:47 PM)iiey Wrote:
(03-26-2025, 08:01 PM)Zbyma72age Wrote: Zainstalowałem i przetestowałem GIMP 3.0.2, działa, ale na razie są dwa problemy
 NDE i HDR nie działają.
Współpracy z innymi programami na razie nie sprawdzałem.
Pozdrowienia i dziękuję

Hi Zbyma72age,
Thank you for testing out, we use the old Google Nik Collection that is still out there on the internet. The "HDR Efex Pro 2" alone has an issue, that it cannot save the processed image back to the input image, you can also test it by running the program directly:
Code:
C:\Program Files\Google\Nik Collection\HDR Efex Pro 2> & './HDR Efex Pro 2.exe' MY_INPUT_IMAGE.jpg
The "Save" button not work, only "Save Image as...", but it breaks the workflow of this plugin, unfortunately.
One stupid workaround is, you could input the path "C:\Users\YOUR_USERNAME\AppData\Local\Temp\tmpNik.jpg" by "Save Image as..." manually and click "Save".


Regarding the issue with "HDR Efex Pro 2", I updated the script (v3.0.3) so that it handles the output of this program automatically.

Side note: you will still have this problem if you configure your ~/Documents path other than windows default setup.
In that case you can either use above workaround or specify your real Documents path in the script here additionally then try again in GIMP (readme with troubleshooting section is updated).
Reply
#22
(03-30-2025, 01:24 PM)iiii Wrote:
(03-26-2025, 10:47 PM)iiii Wrote:
(03-26-2025, 08:01 PM)Zbyma72wiek Wrote: Zainstalowałem i przetestowałem GIMP 3.0.2, działa, ale na razie są dwa problemy
 NDE i HDR nie działają.
Współpracy z innymi programami na razie nie sprawdzałem.
Pozdrowienia i dziękuję

Hi Zbyma72age,
Thank you for testing out, we use the old Google Nik Collection that is still out there on the internet. The "HDR Efex Pro 2" alone has an issue, that it cannot save the processed image back to the input image, you can also test it by running the program directly:
Code:
C:\Program Files\Google\Nik Collection\HDR Efex Pro 2> & './HDR Efex Pro 2.exe' MY_INPUT_IMAGE.jpg
The "Save" button not work, only "Save Image as...", but it breaks the workflow of this plugin, unfortunately.
One stupid workaround is, you could input the path "C:\Users\YOUR_USERNAME\AppData\Local\Temp\tmpNik.jpg" by "Save Image as..." manually and click "Save".


Regarding the issue with "HDR Efex Pro 2", I updated the script (v3.0.3) so that it handles the output of this program automatically.

Side note: you will still have this problem if you configure your ~/Documents path other than windows default setup.
In that case you can either use above workaround or specify your real Documents path in the script here additionally then try again in GIMP (readme with troubleshooting section is updated).
I still do not achieve a positive result.
Maybe the fault lies with me because I did not specify that I have Windows 11 64bit 24h2
and that I am not a computer scientist, but an ordinary user of GIMP 3.0.2-1
Reply
#23
(03-30-2025, 05:35 PM)Zbyma72age Wrote: I still do not achieve a positive result.
Maybe the fault lies with me because I did not specify that I have Windows 11 64bit 24h2
and that I am not a computer scientist, but an ordinary user of GIMP 3.0.2-1

Hi Zbyma72age,
No, the plugin should be written in the way that ordinary users can easily use it. The script may still have bugs or the instruction is not clear enough.


Could you please describe the concrete behavior on your computer when using the "HDR Efex Pro 2" step by step?

Could you confirm that your plugin script has the latest changes from https://github.com/iiey/nikGimp?
Is a layer "HDR Efex Pro 2" created when starting the plugin?
After clicking on "Save" in Nik program, does the layer disappear?
Is there any popup warning message in GIMP? If yes, what is your target location of "Documents", is there a file called tmpNik_HDR.jpg?
E.g.:
   
Reply
#24
[attachment=13306 Wrote:iiii pid='43655' dateline='1743358886']        
(03-30-2025, 05:35 PM)Zbyma72wiek Wrote: Nadal nie osiągnąłem pozytywnego wyniku.
Być może wina leży po mojej stronie, ponieważ nie zaznaczyłem, że mam Windows 11 64bit 24h2
i że nie jestem informatykiem, a zwykłym użytkownikiem GIMP 3.0.2-1

Cześć Zbyma72age,
Nie, wtyczka powinna być napisana w sposób, w jaki zwykli użytkownicy mogą z niej łatwo korzystać. Skrypt może nadal zawierać błędy lub instrukcja nie jest wystarczająco jasna.


Czy mógłbyś opisać konkretne zachowanie na swoim komputerze podczas korzystania z „HDR Efex Pro 2” krok po kroku?

Czy mógłbyś potwierdzić, że skrypt wtyczki zawiera najnowsze zmiany z https://github.com/iiey/nikGimp?
Czy warstwa „HDR Efex Pro 2” jest tworzona podczas uruchamiania wtyczki? Czy
po kliknięciu „Zapisz” w programie Nik warstwa znika?
Czy w GIMP-ie pojawia się jakiś komunikat ostrzegawczy? Jeśli tak, jaka jest docelowa lokalizacja „Dokumentów”, czy jest tam plik o nazwie tmpNik_HDR.jpg?
Np.:
OK


Attached Files Image(s)
   
Reply
#25
Hi Zbyma72age,

Your "Documents" path looks fine, it's default windows path, so you don't need to modify that part of the code.
The issue i see that the plugin try to search for "tmpNik_HDR_HDR.jpg" under Documents/ which doesn't exists, I don't experience that problem even if how many time I ran this plugin.
Cannot figure out what made the script created wrong filename fname.

1. Could you please replace your nikplugin.py with this content:
Code:
#!/usr/bin/env python3

"""
VERSION:
See CHANGELOG for details

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.
This program is licensed under the GNU General Public License v3 (GPLv3).

CONTRIBUTING:
For updates, raising issues and contributing, visit <https://github.com/iiey/nikGimp>.
"""

import gi

gi.require_version("Gimp", "3.0")
gi.require_version("GimpUi", "3.0")
gi.require_version("Gegl", "0.4")

from gi.repository import (
   GLib,
   GObject,
   Gegl,
   Gimp,
   GimpUi,
   Gio,
   Gtk,
)

from enum import Enum
from pathlib import Path
from typing import Any, List, Optional, Tuple, Union

import os
import shutil
import subprocess
import sys
import tempfile
import traceback

# NOTE: Specify IF your installation is not in the default location
# e.g. D:/plugins/nikcollection
NIK_BASE_PATH: str = ""


# Define plug-in metadata
PROC_NAME = "NikCollection"
HELP = "Call an external program"
DOC = "Call an external program passing the active layer as a temp file"
AUTHOR = "nemo"
COPYRIGHT = "GNU General Public License v3"
DATE = "2025-03-30"
VERSION = "3.0.3"


def find_nik_installation() -> Path:
   """Detect Nik Collection installation path based on operating system"""

   possible_paths = []
   # Common installation paths
   if sys.platform == "win32":
       possible_paths = [
           Path("C:/Program Files/Google"),
           Path("C:/Program Files (x86)/Google"),
           Path("C:/Program Files/DxO"),
       ]
   elif sys.platform == "darwin":
       possible_paths = [
           Path("/Applications"),
           Path("~/Applications"),
       ]
   elif sys.platform.startswith("linux"):
       possible_paths = [
           Path.home() / f".wine/drive_c/Program Files/Google",
       ]

   possible_paths = [p / "Nik Collection" for p in possible_paths]
   for path in possible_paths:
       if path.is_dir():
           return path

   # Fallback to user-configured path if specified
   if NIK_BASE_PATH and (nik_path := Path(NIK_BASE_PATH)).is_dir():
       return nik_path

   show_alert(
       text=f"{PROC_NAME} installtion path not found",
       message="Please specify the correct installation path 'NIK_BASE_PATH' in the script.",
   )

   return Path("")


def list_progs(idx: Optional[int] = None) -> Union[List[str], Tuple[str, Path]]:
   """
   Build a list of Nik programs installed on the system
   Args:
       idx: Optional index of the program to return details for
   Returns:
       If idx is None, returns a list of program names
       Otherwise, returns [prog_name, prog_filepath] for the specified program
   """

   def get_exec(prog_dir: Path) -> Optional[Path]:
       """Check for executable in the given directory"""
       if sys.platform == "darwin":
           return prog_dir if prog_dir.suffix == ".app" else None
       else:
           if executables := list(prog_dir.glob("*.exe")):
               return executables[0]
           else:
               return None

   progs_lst = []
   base_path = find_nik_installation()
   if base_path.is_dir():
       # on mac, programs located directly under installation folder
       if sys.platform == "darwin":
           for prog_item in base_path.iterdir():
               if prog_item.is_dir() and prog_item.suffix == ".app":
                   progs_lst.append((prog_item.stem, prog_item))
       else:
           sub_dirs = [d for d in base_path.iterdir() if d.is_dir()]
           for prog_dir in sub_dirs:
               # prefer looking for 64-bit first
               bit64_dirs = [
                   d
                   for d in prog_dir.iterdir()
                   if d.is_dir() and "64-bit" in d.name.lower()
               ]
               exec_file = get_exec(bit64_dirs[0]) if bit64_dirs else None
               # fallback to default version if not found
               if exec_file is None:
                   exec_file = get_exec(prog_dir)
               # only append to final list if one is found
               if exec_file:
                   prog_detail = (prog_dir.name, exec_file)
                   progs_lst.append(prog_detail)
   progs_lst.sort(key=lambda x: x[0].lower())  # sort alphabetically

   if idx is None:
       return [prog[0] for prog in progs_lst]
   elif 0 <= idx < len(progs_lst):
       return progs_lst[idx]
   else:
       return []  # invalid index


def find_hdr_output(prog: str, input_path: Path) -> Optional[Path]:
   """
   Guess output file of 'prog' based on OS
   It typically extends original input file with '_HDR' and stores under the Documents folder
   """

   # NOTE: workaround for troublesome program
   if prog != "HDR Efex Pro 2":
       return None

   Gimp.message(f"5. {input_path=}")
   fname = f"{input_path.stem}_HDR{input_path.suffix}"
   Gimp.message(f"6. {fname=}")
   doc_fname = Path.home() / "Documents" / fname
   Gimp.message(f"7. {doc_fname=} exists: {doc_fname.is_file()}")
   # NOTE: extend paths correspondingly if you custom your documents folder
   if sys.platform in "win32":
       candidate_paths = [
           Path.home() / "Documents",
           Path("D:/Documents"),
       ]
   if sys.platform == "darwin":
       # NOTE: not work, absolute no idea where mac prog saves the output
       candidate_paths = [
           Path.home() / "Documents",
       ]
   elif sys.platform.startswith("linux"):
       wine_user = os.environ.get("USER", os.environ.get("USERNAME", "user"))
       candidate_paths = [
           Path.home() / f".wine/drive_c/users/{wine_user}/My Documents",
       ]

   for path in candidate_paths:
       if (out_path := (path / fname).resolve()).is_file():
           return out_path

   show_alert(
       text=f"{prog} output '{fname}' not found",
       message=f"Plugin cannot identify 'Documents' path on your system.",
   )

   return None


def run_nik(prog_idx: int, gimp_img: Gimp.Image) -> Optional[str]:

   prog_name, prog_filepath = list_progs(prog_idx)
   img_path = os.path.join(tempfile.gettempdir(), f"tmpNik.jpg")
   Gimp.message(f"1. {img_path=}")

   # Save gimp image to disk
   Gimp.progress_init("Saving a copy")
   Gimp.file_save(
       run_mode=Gimp.RunMode.NONINTERACTIVE,
       image=gimp_img,
       file=Gio.File.new_for_path(img_path),
       options=None,
   )
   Gimp.message(f"2. {img_path=}")

   # Invoke external command
   time_before = os.path.getmtime(img_path)
   Gimp.progress_init(f"Calling {prog_name}...")
   Gimp.progress_pulse()
   if sys.platform == "darwin":
       cmd_caller = ["open", "-a"]
   elif sys.platform == "linux":
       cmd_caller = ["wine"]
   else:
       cmd_caller = []
   cmd = cmd_caller + [str(prog_filepath), img_path]
   Gimp.message(f"3. {img_path=}")
   subprocess.check_call(cmd)

   # Move output file to the desinged location so gimp can pick it up
   Gimp.message(f"4. {img_path=}")
   if hdr_path := find_hdr_output(prog_name, Path(img_path)):
       shutil.move(hdr_path, img_path)

   time_after = os.path.getmtime(img_path)

   return None if time_before == time_after else img_path


def show_alert(text: str, message: str, parent=None) -> None:

   dialog = Gtk.MessageDialog(
       transient_for=parent,
       flags=0,
       message_type=Gtk.MessageType.ERROR,
       buttons=Gtk.ButtonsType.CLOSE,
       text=text,
   )
   dialog.format_secondary_text(message)
   dialog.set_title(f"{PROC_NAME} v{VERSION}")
   dialog.run()
   dialog.destroy()


def plugin_main(
   procedure: Gimp.Procedure,
   run_mode: Gimp.RunMode,
   image: Gimp.Image,
   drawables: List[Gimp.Drawable],
   config: Gimp.ProcedureConfig,
   data: Any,
) -> Gimp.ValueArray:
   """
   Main function executed by the plugin. Call an external Nik Collection program on the active layer
   It supports two modes:
     - When visible == 0, operates on the active drawable (current layer).
     - When visible != 0, creates a new layer from the composite of all visible layers
   Workflow:
     - Start an undo group (let user undo all operations as a single step)
     - Copy and save the layer to a temporary file based on the "visible" setting
     - Call the chosen external Nik Collection program
     - Load the modified result into 'image'
     - End the undo group and finalize
   """

   try:
       # Open dialog to get config parameters
       if run_mode == Gimp.RunMode.INTERACTIVE:
           GimpUi.init(PROC_NAME)
           Gegl.init(None)
           dialog = GimpUi.ProcedureDialog(procedure=procedure, config=config)
           dialog.fill(None)
           if not dialog.run():
               dialog.destroy()
               return procedure.new_return_values(
                   Gimp.PDBStatusType.CANCEL,
                   GLib.Error(message="No dialog response"),
               )
           dialog.destroy()

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

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

       # Clear current selection to avoid wrongly pasting the processed image
       if not Gimp.Selection.is_empty(image):
           Gimp.Selection.none(image)

       # Prepare the layer to be processed
       active_layer: Gimp.Layer = image.get_layers()[0]
       if visible == LayerSource.CURRENT_LAYER:
           target_layer = active_layer
       else:
           # prepare a new layer in 'image' from all the visibles
           prog_name: str = list_progs(prog_idx)[0]
           target_layer = Gimp.Layer.new_from_visible(image, image, prog_name)
           image.insert_layer(target_layer, None, 0)

       # Intermediate storage enables exporting content to image then save to disk
       buffer: str = Gimp.edit_named_copy([target_layer], "ShellOutTemp")
       tmp_img: Gimp.Image = Gimp.edit_named_paste_as_new_image(buffer)
       if not tmp_img:
           raise Exception("Failed to create temporary image from buffer")

       Gimp.Image.undo_disable(tmp_img)

       # Execute external program
       tmp_filepath = run_nik(prog_idx, tmp_img)
       if tmp_filepath is None:
           if visible == LayerSource.FROM_VISIBLES:
               image.remove_layer(target_layer)

           tmp_img.delete()
           return procedure.new_return_values(
               Gimp.PDBStatusType.SUCCESS,
               GLib.Error(message="No changes detected"),
           )

       # Put it as a new layer in the opened image
       filtered: Gimp.Layer = Gimp.file_load_layer(
           run_mode=Gimp.RunMode.NONINTERACTIVE,
           image=tmp_img,
           file=Gio.File.new_for_path(tmp_filepath),
       )

       tmp_img.insert_layer(filtered, None, -1)
       buffer: str = Gimp.edit_named_copy([filtered], "ShellOutTemp")

       # Align size and position
       target = active_layer if visible == LayerSource.CURRENT_LAYER else target_layer
       target.resize(filtered.get_width(), filtered.get_height(), 0, 0)
       sel = Gimp.edit_named_paste(target, buffer, True)
       Gimp.Item.transform_translate(
           target,
           (tmp_img.get_width() - filtered.get_width()) / 2,
           (tmp_img.get_height() - filtered.get_height()) / 2,
       )

       target.edit_clear()
       Gimp.buffer_delete(buffer)
       Gimp.floating_sel_anchor(sel)

       # Cleanup temporary file & image
       os.remove(tmp_filepath)
       tmp_img.delete()

       return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
   except Exception as e:
       show_alert(text=str(e), message=traceback.format_exc())
       return procedure.new_return_values(
           Gimp.PDBStatusType.EXECUTION_ERROR,
           GLib.Error(message=f"{str(e)}\n\n{traceback.format_exc()}"),
       )
   finally:
       image.undo_group_end()
       Gimp.context_pop()
       Gimp.displays_flush()


class LayerSource(str, Enum):
   FROM_VISIBLES = "new_from_visibles"
   CURRENT_LAYER = "use_current_layer"

   @classmethod
   def create_choice(cls) -> Gimp.Choice:
       choice = Gimp.Choice.new()
       choice.add(
           nick=cls.FROM_VISIBLES,
           id=1,
           label="new from visible",
           help="Apply filter on new layer created from the visibles",
       )
       choice.add(
           nick=cls.CURRENT_LAYER,
           id=0,
           label="use current layer",
           help="Apply filter directly on the active layer",
       )
       return choice


class NikPlugin(Gimp.PlugIn):

   def do_query_procedures(self):
       return [PROC_NAME]

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

       procedure.set_image_types("RGB*, GRAY*")
       procedure.set_attribution(AUTHOR, COPYRIGHT, DATE)
       procedure.set_documentation(HELP, DOC, None)
       procedure.set_menu_label(PROC_NAME)
       procedure.add_menu_path("<Image>/Filters/")

       # Replace PF_RADIO choice
       visible_choice = LayerSource.create_choice()
       procedure.add_choice_argument(
           name="visible",
           nick="Layer:",
           blurb="Select the layer source",
           choice=visible_choice,
           value=LayerSource.FROM_VISIBLES,
           flags=GObject.ParamFlags.READWRITE,
       )

       # Dropdown selection list of programs
       command_choice = Gimp.Choice.new()
       programs = list_progs()
       for idx, prog in enumerate(programs):
           # the get_property(choice_name) will return 'nick' not 'id' so str(id) to get the index later
           command_choice.add(str(idx), idx, prog, prog)
       procedure.add_choice_argument(
           "command",
           "Program:",
           "Select external program to run",
           command_choice,
           str(idx),
           GObject.ParamFlags.READWRITE,
       )
       return procedure


Gimp.main(NikPlugin.__gtype__, sys.argv)
2. In Gimp, on the upper right corner, click Add Tab > Error Console
Then start using "HDR Efex Pro 2" and take a screenshot of the your console output messages like this example:
   
Reply
#26
(03-30-2025, 09:55 PM)iiii Wrote: Cześć Zbyma72age,

Twoja ścieżka „Documents” wygląda dobrze, to domyślna ścieżka systemu Windows, więc nie musisz modyfikować tej części kodu.
Problem, który widzę, polega na tym, że wtyczka próbuje wyszukać „ tmpNik_HDR_HDR.jpg ” w Documents/, który nie istnieje. Nie mam tego problemu, nawet jeśli uruchomiłem tę wtyczkę wiele razy.
Nie mogę dojść do tego, co spowodowało, że skrypt utworzył złą nazwę pliku fname.

1. Czy mógłbyś zastąpić swój plik nikplugin.py tą zawartością: 2. W programie Gimp, w prawym górnym rogu kliknij Dodaj kartę > Konsola błędów Następnie zacznij używać „HDR Efex Pro 2” i zrób zrzut ekranu komunikatów wyjściowych konsoli, takich jak ten przykład:
Code:
#!/usr/bin/env python3

"""
VERSION:
See CHANGELOG for details

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.
This program is licensed under the GNU General Public License v3 (GPLv3).

CONTRIBUTING:
For updates, raising issues and contributing, visit <https://github.com/iiey/nikGimp>.
"""

import gi

gi.require_version("Gimp", "3.0")
gi.require_version("GimpUi", "3.0")
gi.require_version("Gegl", "0.4")

from gi.repository import (
   GLib,
   GObject,
   Gegl,
   Gimp,
   GimpUi,
   Gio,
   Gtk,
)

from enum import Enum
from pathlib import Path
from typing import Any, List, Optional, Tuple, Union

import os
import shutil
import subprocess
import sys
import tempfile
import traceback

# NOTE: Specify IF your installation is not in the default location
# e.g. D:/plugins/nikcollection
NIK_BASE_PATH: str = ""


# Define plug-in metadata
PROC_NAME = "NikCollection"
HELP = "Call an external program"
DOC = "Call an external program passing the active layer as a temp file"
AUTHOR = "nemo"
COPYRIGHT = "GNU General Public License v3"
DATE = "2025-03-30"
VERSION = "3.0.3"


def find_nik_installation() -> Path:
   """Detect Nik Collection installation path based on operating system"""

   possible_paths = []
   # Common installation paths
   if sys.platform == "win32":
       possible_paths = [
           Path("C:/Program Files/Google"),
           Path("C:/Program Files (x86)/Google"),
           Path("C:/Program Files/DxO"),
       ]
   elif sys.platform == "darwin":
       possible_paths = [
           Path("/Applications"),
           Path("~/Applications"),
       ]
   elif sys.platform.startswith("linux"):
       possible_paths = [
           Path.home() / f".wine/drive_c/Program Files/Google",
       ]

   possible_paths = [p / "Nik Collection" for p in possible_paths]
   for path in possible_paths:
       if path.is_dir():
           return path

   # Fallback to user-configured path if specified
   if NIK_BASE_PATH and (nik_path := Path(NIK_BASE_PATH)).is_dir():
       return nik_path

   show_alert(
       text=f"{PROC_NAME} installtion path not found",
       message="Please specify the correct installation path 'NIK_BASE_PATH' in the script.",
   )

   return Path("")


def list_progs(idx: Optional[int] = None) -> Union[List[str], Tuple[str, Path]]:
   """
   Build a list of Nik programs installed on the system
   Args:
       idx: Optional index of the program to return details for
   Returns:
       If idx is None, returns a list of program names
       Otherwise, returns [prog_name, prog_filepath] for the specified program
   """

   def get_exec(prog_dir: Path) -> Optional[Path]:
       """Check for executable in the given directory"""
       if sys.platform == "darwin":
           return prog_dir if prog_dir.suffix == ".app" else None
       else:
           if executables := list(prog_dir.glob("*.exe")):
               return executables[0]
           else:
               return None

   progs_lst = []
   base_path = find_nik_installation()
   if base_path.is_dir():
       # on mac, programs located directly under installation folder
       if sys.platform == "darwin":
           for prog_item in base_path.iterdir():
               if prog_item.is_dir() and prog_item.suffix == ".app":
                   progs_lst.append((prog_item.stem, prog_item))
       else:
           sub_dirs = [d for d in base_path.iterdir() if d.is_dir()]
           for prog_dir in sub_dirs:
               # prefer looking for 64-bit first
               bit64_dirs = [
                   d
                   for d in prog_dir.iterdir()
                   if d.is_dir() and "64-bit" in d.name.lower()
               ]
               exec_file = get_exec(bit64_dirs[0]) if bit64_dirs else None
               # fallback to default version if not found
               if exec_file is None:
                   exec_file = get_exec(prog_dir)
               # only append to final list if one is found
               if exec_file:
                   prog_detail = (prog_dir.name, exec_file)
                   progs_lst.append(prog_detail)
   progs_lst.sort(key=lambda x: x[0].lower())  # sort alphabetically

   if idx is None:
       return [prog[0] for prog in progs_lst]
   elif 0 <= idx < len(progs_lst):
       return progs_lst[idx]
   else:
       return []  # invalid index


def find_hdr_output(prog: str, input_path: Path) -> Optional[Path]:
   """
   Guess output file of 'prog' based on OS
   It typically extends original input file with '_HDR' and stores under the Documents folder
   """

   # NOTE: workaround for troublesome program
   if prog != "HDR Efex Pro 2":
       return None

   Gimp.message(f"5. {input_path=}")
   fname = f"{input_path.stem}_HDR{input_path.suffix}"
   Gimp.message(f"6. {fname=}")
   doc_fname = Path.home() / "Documents" / fname
   Gimp.message(f"7. {doc_fname=} exists: {doc_fname.is_file()}")
   # NOTE: extend paths correspondingly if you custom your documents folder
   if sys.platform in "win32":
       candidate_paths = [
           Path.home() / "Documents",
           Path("D:/Documents"),
       ]
   if sys.platform == "darwin":
       # NOTE: not work, absolute no idea where mac prog saves the output
       candidate_paths = [
           Path.home() / "Documents",
       ]
   elif sys.platform.startswith("linux"):
       wine_user = os.environ.get("USER", os.environ.get("USERNAME", "user"))
       candidate_paths = [
           Path.home() / f".wine/drive_c/users/{wine_user}/My Documents",
       ]

   for path in candidate_paths:
       if (out_path := (path / fname).resolve()).is_file():
           return out_path

   show_alert(
       text=f"{prog} output '{fname}' not found",
       message=f"Plugin cannot identify 'Documents' path on your system.",
   )

   return None


def run_nik(prog_idx: int, gimp_img: Gimp.Image) -> Optional[str]:

   prog_name, prog_filepath = list_progs(prog_idx)
   img_path = os.path.join(tempfile.gettempdir(), f"tmpNik.jpg")
   Gimp.message(f"1. {img_path=}")

   # Save gimp image to disk
   Gimp.progress_init("Saving a copy")
   Gimp.file_save(
       run_mode=Gimp.RunMode.NONINTERACTIVE,
       image=gimp_img,
       file=Gio.File.new_for_path(img_path),
       options=None,
   )
   Gimp.message(f"2. {img_path=}")

   # Invoke external command
   time_before = os.path.getmtime(img_path)
   Gimp.progress_init(f"Calling {prog_name}...")
   Gimp.progress_pulse()
   if sys.platform == "darwin":
       cmd_caller = ["open", "-a"]
   elif sys.platform == "linux":
       cmd_caller = ["wine"]
   else:
       cmd_caller = []
   cmd = cmd_caller + [str(prog_filepath), img_path]
   Gimp.message(f"3. {img_path=}")
   subprocess.check_call(cmd)

   # Move output file to the desinged location so gimp can pick it up
   Gimp.message(f"4. {img_path=}")
   if hdr_path := find_hdr_output(prog_name, Path(img_path)):
       shutil.move(hdr_path, img_path)

   time_after = os.path.getmtime(img_path)

   return None if time_before == time_after else img_path


def show_alert(text: str, message: str, parent=None) -> None:

   dialog = Gtk.MessageDialog(
       transient_for=parent,
       flags=0,
       message_type=Gtk.MessageType.ERROR,
       buttons=Gtk.ButtonsType.CLOSE,
       text=text,
   )
   dialog.format_secondary_text(message)
   dialog.set_title(f"{PROC_NAME} v{VERSION}")
   dialog.run()
   dialog.destroy()


def plugin_main(
   procedure: Gimp.Procedure,
   run_mode: Gimp.RunMode,
   image: Gimp.Image,
   drawables: List[Gimp.Drawable],
   config: Gimp.ProcedureConfig,
   data: Any,
) -> Gimp.ValueArray:
   """
   Main function executed by the plugin. Call an external Nik Collection program on the active layer
   It supports two modes:
     - When visible == 0, operates on the active drawable (current layer).
     - When visible != 0, creates a new layer from the composite of all visible layers
   Workflow:
     - Start an undo group (let user undo all operations as a single step)
     - Copy and save the layer to a temporary file based on the "visible" setting
     - Call the chosen external Nik Collection program
     - Load the modified result into 'image'
     - End the undo group and finalize
   """

   try:
       # Open dialog to get config parameters
       if run_mode == Gimp.RunMode.INTERACTIVE:
           GimpUi.init(PROC_NAME)
           Gegl.init(None)
           dialog = GimpUi.ProcedureDialog(procedure=procedure, config=config)
           dialog.fill(None)
           if not dialog.run():
               dialog.destroy()
               return procedure.new_return_values(
                   Gimp.PDBStatusType.CANCEL,
                   GLib.Error(message="No dialog response"),
               )
           dialog.destroy()

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

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

       # Clear current selection to avoid wrongly pasting the processed image
       if not Gimp.Selection.is_empty(image):
           Gimp.Selection.none(image)

       # Prepare the layer to be processed
       active_layer: Gimp.Layer = image.get_layers()[0]
       if visible == LayerSource.CURRENT_LAYER:
           target_layer = active_layer
       else:
           # prepare a new layer in 'image' from all the visibles
           prog_name: str = list_progs(prog_idx)[0]
           target_layer = Gimp.Layer.new_from_visible(image, image, prog_name)
           image.insert_layer(target_layer, None, 0)

       # Intermediate storage enables exporting content to image then save to disk
       buffer: str = Gimp.edit_named_copy([target_layer], "ShellOutTemp")
       tmp_img: Gimp.Image = Gimp.edit_named_paste_as_new_image(buffer)
       if not tmp_img:
           raise Exception("Failed to create temporary image from buffer")

       Gimp.Image.undo_disable(tmp_img)

       # Execute external program
       tmp_filepath = run_nik(prog_idx, tmp_img)
       if tmp_filepath is None:
           if visible == LayerSource.FROM_VISIBLES:
               image.remove_layer(target_layer)

           tmp_img.delete()
           return procedure.new_return_values(
               Gimp.PDBStatusType.SUCCESS,
               GLib.Error(message="No changes detected"),
           )

       # Put it as a new layer in the opened image
       filtered: Gimp.Layer = Gimp.file_load_layer(
           run_mode=Gimp.RunMode.NONINTERACTIVE,
           image=tmp_img,
           file=Gio.File.new_for_path(tmp_filepath),
       )

       tmp_img.insert_layer(filtered, None, -1)
       buffer: str = Gimp.edit_named_copy([filtered], "ShellOutTemp")

       # Align size and position
       target = active_layer if visible == LayerSource.CURRENT_LAYER else target_layer
       target.resize(filtered.get_width(), filtered.get_height(), 0, 0)
       sel = Gimp.edit_named_paste(target, buffer, True)
       Gimp.Item.transform_translate(
           target,
           (tmp_img.get_width() - filtered.get_width()) / 2,
           (tmp_img.get_height() - filtered.get_height()) / 2,
       )

       target.edit_clear()
       Gimp.buffer_delete(buffer)
       Gimp.floating_sel_anchor(sel)

       # Cleanup temporary file & image
       os.remove(tmp_filepath)
       tmp_img.delete()

       return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
   except Exception as e:
       show_alert(text=str(e), message=traceback.format_exc())
       return procedure.new_return_values(
           Gimp.PDBStatusType.EXECUTION_ERROR,
           GLib.Error(message=f"{str(e)}\n\n{traceback.format_exc()}"),
       )
   finally:
       image.undo_group_end()
       Gimp.context_pop()
       Gimp.displays_flush()


class LayerSource(str, Enum):
   FROM_VISIBLES = "new_from_visibles"
   CURRENT_LAYER = "use_current_layer"

   @classmethod
   def create_choice(cls) -> Gimp.Choice:
       choice = Gimp.Choice.new()
       choice.add(
           nick=cls.FROM_VISIBLES,
           id=1,
           label="new from visible",
           help="Apply filter on new layer created from the visibles",
       )
       choice.add(
           nick=cls.CURRENT_LAYER,
           id=0,
           label="use current layer",
           help="Apply filter directly on the active layer",
       )
       return choice


class NikPlugin(Gimp.PlugIn):

   def do_query_procedures(self):
       return [PROC_NAME]

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

       procedure.set_image_types("RGB*, GRAY*")
       procedure.set_attribution(AUTHOR, COPYRIGHT, DATE)
       procedure.set_documentation(HELP, DOC, None)
       procedure.set_menu_label(PROC_NAME)
       procedure.add_menu_path("<Image>/Filters/")

       # Replace PF_RADIO choice
       visible_choice = LayerSource.create_choice()
       procedure.add_choice_argument(
           name="visible",
           nick="Layer:",
           blurb="Select the layer source",
           choice=visible_choice,
           value=LayerSource.FROM_VISIBLES,
           flags=GObject.ParamFlags.READWRITE,
       )

       # Dropdown selection list of programs
       command_choice = Gimp.Choice.new()
       programs = list_progs()
       for idx, prog in enumerate(programs):
           # the get_property(choice_name) will return 'nick' not 'id' so str(id) to get the index later
           command_choice.add(str(idx), idx, prog, prog)
       procedure.add_choice_argument(
           "command",
           "Program:",
           "Select external program to run",
           command_choice,
           str(idx),
           GObject.ParamFlags.READWRITE,
       )
       return procedure


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


Attached Files
.pdf   Info.pdf (Size: 169.6 KB / Downloads: 6)
Reply
#27
Hi Zbyma72age,

- It doesn't matter that you have win version 24H2 or 64bit architecture, about win32 see python doc
- The intermediate file supposes to have that name, don't change.
=> please DO NOT modify anything not mentioned when you don't understand them!
   

Just copy the plugin script from my repo (not the debugging code in previous post) but don't touch anything this time then you should good to go.
Reply
#28
Hi all,

I update the plugin to nikGimp v3.0.4 (see CHANGELOG).
- Issue with HDR Efex Pro 2 was fixed, filter should work like others
- Simplified installation steps (no need to specify installation path if using default).
- Not thoroughly tested, but theoretically it should work with Linux+Wine and Mac now.
Reply
#29
(03-31-2025, 05:21 PM)iiey Wrote: Hi all,

I update the plugin to nikGimp v3.0.4 (see CHANGELOG).
- Issue with HDR Efex Pro 2 was fixed, filter should work like others
- Simplified installation steps (no need to specify installation path if using default).
- Not thoroughly tested, but theoretically it should work with Linux+Wine and Mac now.

I saved the new version 3.0.4.
Now after clicking Save
The only effect is that the HDR Efex Pro 2 layer disappears
No effect
Reply
#30
(03-31-2025, 09:06 PM)Zbyma72age Wrote: I saved the new version 3.0.4.
Now after clicking Save
The only effect is that the HDR Efex Pro 2 layer disappears
No effect

1. Was a new image created in your "Documents" when clicking on "Save"?

2. Which NikCollection version do you use?

3. Normally in your computer, when opening an image directly with the HDR program and clicking on "Save" where does the processed image go to?
Reply


Forum Jump: