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:
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:
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)
Then start using "HDR Efex Pro 2" and take a screenshot of the your console output messages like this example: