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
#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


Messages In This Thread
RE: Converting python plugin-in shellout.py from gimp 2.x to 3.x - by Zbyma72age - 03-31-2025, 12:39 PM

Forum Jump: