public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/darktable] rawhide: 5.6.1 release
@ 2026-09-01 8:46 Germano Massullo
0 siblings, 0 replies; only message in thread
From: Germano Massullo @ 2026-09-01 8:46 UTC (permalink / raw)
To: git-commits
A new commit has been pushed.
Repo : rpms/darktable
Branch : rawhide
Commit : 6fa39d4362d90089967b92a6285a555ac4630026
Author : Germano Massullo (Thetra) <germano.massullo@thetra.eu>
Date : 2026-09-01T10:45:10+02:00
Stats : +3091/-398 in 9 file(s)
URL : https://src.fedoraproject.org/rpms/darktable/c/6fa39d4362d90089967b92a6285a555ac4630026?branch=rawhide
Log:
5.6.1 release
---
diff --git a/.gitignore b/.gitignore
index d0449dc..7edf7f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,3 +79,5 @@
/darktable-5.2.1.tar.xz
/darktable-5.4.0.tar.xz
/darktable-5.4.1.tar.xz
+/darktable-5.6.0.tar.xz
+/darktable-5.6.1.tar.xz
diff --git a/0001-Add-5.4.1-in-appdata.patch b/0001-Add-5.4.1-in-appdata.patch
deleted file mode 100644
index b3c5033..0000000
--- a/0001-Add-5.4.1-in-appdata.patch
+++ /dev/null
@@ -1,21 +0,0 @@
-From 1435feaec30ea1acbf94aaec27ce7507333ed2d7 Mon Sep 17 00:00:00 2001
-From: Pascal Obry <pascal@obry.net>
-Date: Thu, 5 Feb 2026 17:56:21 +0100
-Subject: [PATCH] Add 5.4.1 in appdata.
-
----
- data/org.darktable.darktable.appdata.xml.in | 1 +
- 1 file changed, 1 insertion(+)
-
-diff --git a/data/org.darktable.darktable.appdata.xml.in b/data/org.darktable.darktable.appdata.xml.in
-index 600c87b6c1e4..1a45de1eb74d 100644
---- a/data/org.darktable.darktable.appdata.xml.in
-+++ b/data/org.darktable.darktable.appdata.xml.in
-@@ -47,6 +47,7 @@
- </screenshots>
- <content_rating type="oars-1.1" />
- <releases>
-+ <release date="2026-02-05" version="5.4.1"/>
- <release date="2025-12-21" version="5.4.0"/>
- <release date="2025-08-06" version="5.2.1"/>
- <release date="2025-06-21" version="5.2.0"/>
diff --git a/8dc68e2a4551f03838b7f2da9276e5d1eb20bca0.patch b/8dc68e2a4551f03838b7f2da9276e5d1eb20bca0.patch
deleted file mode 100644
index 90e1ce7..0000000
--- a/8dc68e2a4551f03838b7f2da9276e5d1eb20bca0.patch
+++ /dev/null
@@ -1,53 +0,0 @@
-From 8dc68e2a4551f03838b7f2da9276e5d1eb20bca0 Mon Sep 17 00:00:00 2001
-From: Alex Tutubalin <lexa@lexa.ru>
-Date: Sun, 25 Jan 2026 19:56:24 +0300
-Subject: [PATCH] Use LIBRAW_EXCEPTION instead of own internal in
- losslessjpeg.h
-
----
- internal/losslessjpeg.h | 11 +++--------
- 1 file changed, 3 insertions(+), 8 deletions(-)
-
-diff --git a/internal/losslessjpeg.h b/internal/losslessjpeg.h
-index 7964822d..eced82b3 100644
---- a/src/external/LibRaw/internal/losslessjpeg.h
-+++ b/src/external/LibRaw/internal/losslessjpeg.h
-@@ -20,6 +20,7 @@ it under the terms of the one of two licenses as you choose:
- #pragma once
- #include <stdint.h>
- #include <vector>
-+#include "libraw_cxx_defs.h"
-
- struct BitPump // generic bit source
- {
-@@ -37,12 +38,6 @@ struct BitPump // generic bit source
-
- struct ByteStreamBE // Jpeg is always big endian
- {
-- enum Exceptions
-- {
-- OK = 0,
-- EndOfBuffer = 1
-- };
--
- uint8_t *buffer;
- unsigned size, pos;
- ByteStreamBE(uint8_t *b, unsigned s) : buffer(b), size(s), pos(0) {}
-@@ -52,7 +47,7 @@ struct ByteStreamBE // Jpeg is always big endian
- uint8_t get_u8()
- {
- if (pos >= size)
-- throw EndOfBuffer;
-+ throw LIBRAW_EXCEPTION_IO_CORRUPT;
- uint8_t ret = buffer[pos];
- pos++;
- return ret;
-@@ -60,7 +55,7 @@ struct ByteStreamBE // Jpeg is always big endian
- uint16_t get_u16()
- {
- if (pos + 2 > size)
-- throw EndOfBuffer;
-+ throw LIBRAW_EXCEPTION_IO_CORRUPT;
- uint8_t r1 = buffer[pos];
- uint8_t r2 = buffer[pos + 1];
- pos += 2;
diff --git a/cve-2026-5318.patch b/cve-2026-5318.patch
deleted file mode 100644
index 0be8543..0000000
--- a/cve-2026-5318.patch
+++ /dev/null
@@ -1,27 +0,0 @@
-diff -urNr darktable-5.4.1-orig/src/external/LibRaw/src/decompressors/losslessjpeg.cpp darktable-5.4.1/src/external/LibRaw/src/decompressors/losslessjpeg.cpp
---- darktable-5.4.1-orig/src/external/LibRaw/src/decompressors/losslessjpeg.cpp 2026-01-09 10:59:59.000000000 +0100
-+++ darktable-5.4.1/src/external/LibRaw/src/decompressors/losslessjpeg.cpp 2026-04-02 23:23:27.630825281 +0200
-@@ -361,6 +361,7 @@
- nbits--;
- }
- hufftable.resize( size_t(1ULL << nbits));
-+ uint32_t tsize = 1 << nbits;
- for (unsigned i = 0; i < hufftable.size(); i++) hufftable[i] = 0;
-
- int h = 0;
-@@ -371,10 +372,14 @@
- {
- for (int j = 0; j < (1 << (nbits - len - 1)); j++)
- {
-- hufftable[h] = ((len+1) << 16) | (uint8_t(huffval[pos] & 0xff) << 8) | uint8_t(shiftval[pos] & 0xff);
-+ if (h >= tsize)
-+ throw LIBRAW_EXCEPTION_IO_CORRUPT;
-+ hufftable[h] = ((len + 1) << 16) | (uint8_t(huffval[pos] & 0xff) << 8) | uint8_t(shiftval[pos] & 0xff);
- h++;
- }
- pos++;
-+ if(pos >= 256)
-+ throw LIBRAW_EXCEPTION_IO_CORRUPT;
- }
- }
- if (!disable_cache)
diff --git a/darktable-5.4.1-lua-5.5-scripts.patch b/darktable-5.4.1-lua-5.5-scripts.patch
deleted file mode 100644
index 752b6a2..0000000
--- a/darktable-5.4.1-lua-5.5-scripts.patch
+++ /dev/null
@@ -1,136 +0,0 @@
---- darktable-5.4.1/src/external/lua-scripts/contrib/HDRMerge.lua 2025-11-14 20:04:29.000000000 -0500
-+++ darktable-5.4.1/src/external/lua-scripts/contrib/HDRMerge.lua 2026-05-18 11:41:31.804560353 -0400
-@@ -277,9 +277,9 @@ local function main()
- local set_tag = GUI.Target.add_tags.text
- if set_tag ~= nil then -- add additional user-specified tags
- for tag in string.gmatch(set_tag, '[^,]+') do
-- tag = CleanSpaces(tag)
-- tag = dt.tags.create(tag)
-- dt.tags.attach(tag, imported)
-+ local cleaned_tag = CleanSpaces(tag)
-+ cleaned_tag = dt.tags.create(cleaned_tag)
-+ dt.tags.attach(cleaned_tag, imported)
- end
- end
- dt.print(_('HDRMerge completed successfully'))
---- darktable-5.4.1/src/external/lua-scripts/contrib/enfuseAdvanced.lua 2025-11-14 20:04:29.000000000 -0500
-+++ darktable-5.4.1/src/external/lua-scripts/contrib/enfuseAdvanced.lua 2026-05-18 11:41:31.805560362 -0400
-@@ -520,9 +520,9 @@ local function main(storage, image_table
- local set_tag = GUI.Target.add_tags.text
- if set_tag ~= nil then --add additional user-specified tags
- for tag in string.gmatch(set_tag, '[^,]+') do
-- tag = CleanSpaces(tag)
-- tag = dt.tags.create(tag)
-- dt.tags.attach(tag, imported)
-+ local cleaned_tag = CleanSpaces(tag)
-+ cleaned_tag = dt.tags.create(cleaned_tag)
-+ dt.tags.attach(cleaned_tag, imported)
- end
- end
- end
---- darktable-5.4.1/src/external/lua-scripts/contrib/face_recognition.lua 2025-11-14 20:04:29.000000000 -0500
-+++ darktable-5.4.1/src/external/lua-scripts/contrib/face_recognition.lua 2026-05-18 11:41:31.800560315 -0400
-@@ -311,25 +311,26 @@ local function face_recognition ()
- if ignoreByTag (img, ignoreTags) then
- dt.print_log("Face recognition: Ignoring image with ID " .. img.id)
- else
-+ local tag_name = t
- -- Check of unrecognized unknown_person
-- if t == "unknown_person" then
-- t = unknownTag
-+ if tag_name == "unknown_person" then
-+ tag_name = unknownTag
- end
- -- Check of unrecognized no_persons_found
-- if t == "no_persons_found" then
-- t = nonpersonsfoundTag
-+ if tag_name == "no_persons_found" then
-+ tag_name = nonpersonsfoundTag
- end
-- if t ~= "" and t ~= nil then
-- if categoryTagString ~= "" and t ~= nonpersonsfoundTag then
-- t = categoryTagString .. "|" .. t
-+ if tag_name ~= "" and tag_name ~= nil then
-+ if categoryTagString ~= "" and tag_name ~= nonpersonsfoundTag then
-+ tag_name = categoryTagString .. "|" .. tag_name
- end
-- dt.print_log ("ImgId:" .. img.id .. " Tag:".. t)
-+ dt.print_log ("ImgId:" .. img.id .. " Tag:".. tag_name)
- -- Create tag if it does not exist
-- if tags_list[t] == nil then
-- tag = dt.tags.create (t)
-- tags_list[t] = tag
-+ if tags_list[tag_name] == nil then
-+ tag = dt.tags.create (tag_name)
-+ tags_list[tag_name] = tag
- else
-- tag = tags_list[t]
-+ tag = tags_list[tag_name]
- end
- img:attach_tag (tag)
- end
---- darktable-5.4.1/src/external/lua-scripts/contrib/gimp.lua 2025-11-14 20:04:29.000000000 -0500
-+++ darktable-5.4.1/src/external/lua-scripts/contrib/gimp.lua 2026-05-18 11:41:31.801560324 -0400
-@@ -142,8 +142,8 @@ local function gimp_edit(storage, image_
- img_list = ""
-
- for _,exp_img in pairs(image_table) do
-- exp_img = df.sanitize_filename(exp_img)
-- img_list = img_list ..exp_img.. " "
-+ local sanitized = df.sanitize_filename(exp_img)
-+ img_list = img_list ..sanitized.. " "
- end
-
- dt.print(_("launching GIMP..."))
---- darktable-5.4.1/src/external/lua-scripts/contrib/image_stack.lua 2025-11-14 20:04:29.000000000 -0500
-+++ darktable-5.4.1/src/external/lua-scripts/contrib/image_stack.lua 2026-05-18 11:41:31.803560343 -0400
-@@ -323,8 +323,8 @@ local function cleanup(img_list)
- dt.print_log("image list is " .. img_list)
- files = du.split(img_list, " ")
- for _,f in ipairs(files) do
-- f = string.gsub(f, '[\'\"]', "")
-- os.remove(f)
-+ local cleaned = string.gsub(f, '[\'\"]', "")
-+ os.remove(cleaned)
- end
- end
-
---- darktable-5.4.1/src/external/lua-scripts/tools/get_lib_manpages.lua 2025-11-14 20:04:29.000000000 -0500
-+++ darktable-5.4.1/src/external/lua-scripts/tools/get_lib_manpages.lua 2026-05-18 11:41:31.801560324 -0400
-@@ -63,8 +63,8 @@ local output = io.popen("cd "..dt.config
- -- loop through the libraries
-
- for line in output:lines() do
-- line = string.gsub(line, "/", ".")
-- local lib_name = line:sub(3,-5)
-+ local cleaned_line = string.gsub(line, "/", ".")
-+ local lib_name = cleaned_line:sub(3,-5)
- if lib_name:len() > 2 then
- lib_name = "lib/" .. lib_name
- local lib = require(lib_name)
---- darktable-5.4.1/src/external/lua-scripts/tools/get_libdoc.lua 2025-11-14 20:04:29.000000000 -0500
-+++ darktable-5.4.1/src/external/lua-scripts/tools/get_libdoc.lua 2026-05-18 11:41:31.803560343 -0400
-@@ -40,8 +40,8 @@ local output = io.popen("cd "..dt.config
- -- loop through the libraries
-
- for line in output:lines() do
-- line = string.gsub(line, "/", ".")
-- local lib_name = line:sub(3,-5)
-+ local cleaned_line = string.gsub(line, "/", ".")
-+ local lib_name = cleaned_line:sub(3,-5)
- if lib_name:len() > 2 then
- lib_name = "lib/" .. lib_name
- local lib = require(lib_name)
---- darktable-5.4.1/src/external/lua-scripts/tools/script_manager.lua 2025-11-14 20:04:29.000000000 -0500
-+++ darktable-5.4.1/src/external/lua-scripts/tools/script_manager.lua 2026-05-18 11:41:31.799560306 -0400
-@@ -894,7 +894,9 @@ local function install_scripts()
- sm.widgets.folder_selector.selected = i
- break
- end
-- i = i + 1
-+ -- Lua 5.5: for loop control variables are read-only. This increment
-+ -- was always a no-op since numeric for loops use an internal counter.
-+ -- i = i + 1
- end
-
- log.msg(log.debug, "clearing text fields")
diff --git a/darktable-5.4.1-lua-5.5.patch b/darktable-5.4.1-lua-5.5.patch
deleted file mode 100644
index fd5ef0e..0000000
--- a/darktable-5.4.1-lua-5.5.patch
+++ /dev/null
@@ -1,88 +0,0 @@
-diff -up darktable-5.4.1/src/CMakeLists.txt.lua55 darktable-5.4.1/src/CMakeLists.txt
---- darktable-5.4.1/src/CMakeLists.txt.lua55 2026-02-27 15:08:42.755695468 -0500
-+++ darktable-5.4.1/src/CMakeLists.txt 2026-02-27 15:08:49.905193366 -0500
-@@ -706,14 +706,14 @@ configure_file("${CMAKE_CURRENT_SOURCE_D
- # Build external deps
- #
- if(USE_LUA)
-- find_package(Lua54 5.4)
-- if(Lua54_FOUND)
-+ find_package(Lua55 5.5)
-+ if(Lua55_FOUND)
- message(STATUS "Lua support: Enabled")
-- include_directories(SYSTEM ${Lua54_INCLUDE_DIRS})
-- list(APPEND LIBS ${Lua54_LDFLAGS})
-+ include_directories(SYSTEM ${Lua55_INCLUDE_DIRS})
-+ list(APPEND LIBS ${Lua55_LDFLAGS})
- else()
- if(DONT_USE_INTERNAL_LUA)
-- message(STATUS "Lua support: System library for lua 5.4 not found (to use darktable's version use -DDONT_USE_INTERNAL_LUA=Off)")
-+ message(STATUS "Lua support: System library for lua 5.5 not found (to use darktable's version use -DDONT_USE_INTERNAL_LUA=Off)")
- set(USE_LUA OFF)
- else(DONT_USE_INTERNAL_LUA)
- message(STATUS "Lua support: System library not found (using darktable's version)")
-@@ -775,9 +775,9 @@ else(USE_LUA)
- endif(USE_LUA)
-
- if(USE_LUA)
-- if(NOT Lua54_FOUND)
-+ if(NOT Lua55_FOUND)
- list(APPEND LIB_DEPS lua) # 2nd half of the ugliness :/
-- endif(NOT Lua54_FOUND)
-+ endif(NOT Lua55_FOUND)
- # liblautoc for lua automated interface generation
- add_library(lautoc_static STATIC IMPORTED)
- set_target_properties(lautoc_static PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/external/LuaAutoC/liblautoc.a POSITION_INDEPENDENT_CODE True)
-diff -up darktable-5.4.1/cmake/modules/FindLua55.cmake.lua55 darktable-5.4.1/cmake/modules/FindLua55.cmake
---- darktable-5.4.1/cmake/modules/FindLua55.cmake.lua55 2026-02-27 15:13:49.037575029 -0500
-+++ darktable-5.4.1/cmake/modules/FindLua55.cmake 2026-02-27 15:12:59.942533739 -0500
-@@ -0,0 +1,49 @@
-+SET(Lua_VERSION_TOO_HIGH 5.6)
-+
-+include(Prebuilt)
-+include(LibFindMacros)
-+
-+libfind_pkg_search_module(Lua55 lua55 lua5.5 lua-5.5 lua)
-+
-+if(Lua55_FIND_VERSION)
-+ cmake_minimum_required(VERSION 3.10.0)
-+ set(Lua55_FAILED_VERSION_CHECK true)
-+
-+ if(Lua55_FIND_VERSION_EXACT)
-+ if(Lua55_VERSION VERSION_EQUAL Lua55_FIND_VERSION)
-+ set(Lua55_FAILED_VERSION_CHECK false)
-+ endif()
-+ else()
-+ if(Lua55_VERSION VERSION_EQUAL Lua55_FIND_VERSION OR
-+ Lua55_VERSION VERSION_GREATER Lua55_FIND_VERSION AND
-+ Lua55_VERSION VERSION_LESS Lua_VERSION_TOO_HIGH)
-+ set(Lua55_FAILED_VERSION_CHECK false)
-+ endif()
-+ endif()
-+
-+ if(Lua55_FAILED_VERSION_CHECK)
-+ if(Lua55_FIND_REQUIRED AND NOT Lua55_FIND_QUIETLY)
-+ if(Lua55_FIND_VERSION_EXACT)
-+ message(FATAL_ERROR "Lua5.5 version check failed. Version ${Lua55_VERSION} was found, version ${Lua55_FIND_VERSION} is needed exactly.")
-+ else(Lua55_FIND_VERSION_EXACT)
-+ message(FATAL_ERROR "Lua5.5 version check failed. Version ${Lua55_VERSION} was found, at least version ${Lua55_FIND_VERSION} is required")
-+ endif(Lua55_FIND_VERSION_EXACT)
-+ endif(Lua55_FIND_REQUIRED AND NOT Lua55_FIND_QUIETLY)
-+
-+ set(Lua55_FOUND false)
-+ endif(Lua55_FAILED_VERSION_CHECK)
-+
-+endif(Lua55_FIND_VERSION)
-+
-+if(Lua55_FOUND)
-+ set(Lua55 ON CACHE BOOL "Build with lua5.5 support.")
-+ if(APPLE)
-+ foreach(i ${Lua55_LIBRARIES})
-+ find_library(_lua55_LIBRARY NAMES ${i} HINTS ${Lua55_LIBRARY_DIRS})
-+ list(APPEND Lua55_LIBRARIES_FULL ${_lua55_LIBRARY})
-+ unset(_lua55_LIBRARY CACHE)
-+ endforeach(i)
-+ set(Lua55_LIBRARIES ${Lua55_LIBRARIES_FULL})
-+ unset(Lua55_LIBRARIES_FULL CACHE)
-+ endif()
-+endif(Lua55_FOUND)
diff --git a/darktable.spec b/darktable.spec
index 3bdf43d..78b46ae 100644
--- a/darktable.spec
+++ b/darktable.spec
@@ -6,7 +6,7 @@
###
Name: darktable
-Version: 5.4.1
+Version: 5.6.1
Release: %autorelease
Summary: Utility to organize and develop raw images
@@ -18,16 +18,8 @@ Source0: https://github.com/darktable-org/darktable/releases/download/release-%{
#Source1: https://github.com/darktable-org/darktable/releases/download/release-%%{version}/%%{name}-%%{version}.tar.xz.asc
#Source2: https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xf10f9686652b0e949fcd94c318dca123f949bd3b
-Patch0: 0001-Add-5.4.1-in-appdata.patch
-
-Patch1: darktable-5.4.1-lua-5.5.patch
-# https://bugzilla.redhat.com/show_bug.cgi?id=2454222
-Patch2: cve-2026-5318.patch
-# This upstream change is necessary for the CVE-2026-5318 patch above to actually find the enum values
-# https://github.com/LibRaw/LibRaw/commit/8dc68e2a4551f03838b7f2da9276e5d1eb20bca0
-Patch3: 8dc68e2a4551f03838b7f2da9276e5d1eb20bca0.patch
# https://github.com/darktable-org/lua-scripts/pull/678
-Patch4: darktable-5.4.1-lua-5.5-scripts.patch
+Patch0: lua.patch
BuildRequires: cairo-devel
# clang is optional (OpenCL kernel build test)
@@ -38,17 +30,9 @@ BuildRequires: colord-devel
BuildRequires: cups-devel
BuildRequires: desktop-file-utils
BuildRequires: exiv2-devel >= 0.27.2
-%if %{defined fedora}
BuildRequires: gcc
-%endif
-%if 0%{?rhel}
-BuildRequires: gcc-toolset-12
-#BuildRequires: gcc-toolset-12-gcc
-#BuildRequires: gcc-toolset-12-annobin-plugin-gcc
-%endif
-%if %{defined fedora}
-BuildRequires: gmic-devel
-%endif
+BuildRequires: glib2-devel >= 2.56
+BuildRequires: gmic-devel >= 2.7.0
BuildRequires: GraphicsMagick-devel
BuildRequires: gtk3-devel >= 3.24.15
BuildRequires: intltool
@@ -60,10 +44,8 @@ BuildRequires: lensfun-devel
BuildRequires: libappstream-glib
BuildRequires: cmake(libavif) >= 0.9.3
BuildRequires: libcurl-devel >= 7.56
-BuildRequires: libgphoto2-devel >= 2.4.5
-%if ((%{defined rhel} && 0%{?rhel} >= 9) || %{defined fedora})
+BuildRequires: libgphoto2-devel >= 2.5
BuildRequires: libheif-devel >= 1.13.0
-%endif
BuildRequires: libicu-devel
BuildRequires: libjpeg-devel
BuildRequires: libjxl-devel >= 0.7.0
@@ -71,24 +53,20 @@ BuildRequires: libpng-devel >= 1.5.0
BuildRequires: librsvg2-devel >= 2.26
BuildRequires: libsecret-devel
BuildRequires: libtiff-devel
-BuildRequires: libwebp-devel
+BuildRequires: libwebp-devel >= 0.3.0
# llvm-devel is optional (OpenCL kernel build test)
BuildRequires: llvm-devel >= 7
-%if (%{defined rhel} && 0%{?rhel} >= 9) || %{defined fedora}
BuildRequires: pkgconfig(lua)
-%endif
# opencl-headers is optional (OpenCL kernel build test)
BuildRequires: opencl-headers
-%if (%{defined rhel} && 0%{?rhel} >= 9) || %{defined fedora}
BuildRequires: cmake(OpenEXR) > 3.0
-BuildRequires: cmake(Imath)
-%else
-BuildRequires: OpenEXR-devel
+BuildRequires: cmake(Imath) >= 3.1.0
+%if (%{defined fedora} && 0%{?fedora} > 43)
+BuildRequires: libarchive-devel >= 3.8.5
+BuildRequires: onnxruntime-devel >= 1.18
%endif
BuildRequires: openjpeg2-devel
-%if %{defined fedora}
BuildRequires: osm-gps-map-devel >= 1.0
-%endif
BuildRequires: perl-interpreter
BuildRequires: perl(FindBin)
BuildRequires: perl(lib)
@@ -96,9 +74,10 @@ BuildRequires: pkgconfig >= 0.22
BuildRequires: po4a
BuildRequires: perl-podlators
BuildRequires: portmidi-devel
-BuildRequires: pugixml-devel >= 1.5
+BuildRequires: potrace-devel >= 1.16
+BuildRequires: pugixml-devel >= 1.8
BuildRequires: cmake(SDL2)
-BuildRequires: sqlite-devel
+BuildRequires: sqlite-devel >= 3.26
BuildRequires: zlib-devel >= 1.2.11
Requires: iso-codes >= 3.66
@@ -108,9 +87,6 @@ Requires: iso-codes >= 3.66
Provides: bundled(rawspeed)
# https://bugzilla.redhat.com/show_bug.cgi?id=2252432
Provides: bundled(libraw)
-%if %{defined rhel} && 0%{?rhel} == 8
-Provides: bundled(lua)
-%endif
# Unsupported CPU architectures
# filled https://bugzilla.redhat.com/show_bug.cgi?id=2038684
@@ -159,35 +135,18 @@ sed -i -e 's, \"external/CL/\*\.h\" , ,' src/CMakeLists.txt
%build
-
-#
-# Germano Massullo: I wanted to use %%elseif but it is not yet active in
-# Fedora, etc., despite is supported upstream. I did not compare the Fedora RPM version
-# but I empirically verified that %%elseif and %%elif do not work here, even if you don't get
-# errors during builds
-# https://github.com/rpm-software-management/rpm/issues/311
-# https://github.com/debbuild/debbuild/issues/182
-#
-#
-#%%if %%{defined rhel}
-%if %{defined rhel}
-. /opt/rh/gcc-toolset-12/enable
-%endif
-%if (%{defined rhel} && 0%{?rhel} == 8)
-mkdir %{_target_platform}
-pushd %{_target_platform}
+%if (%{defined fedora} && 0%{?fedora} > 43)
%cmake \
-DCMAKE_LIBRARY_PATH:PATH=%{_libdir} \
-DUSE_GEO:BOOLEAN=ON \
-DCMAKE_BUILD_TYPE:STRING=Release \
-DBINARY_PACKAGE_BUILD=1 \
- -DDONT_USE_INTERNAL_LUA=OFF \
-DBUILD_NOISE_TOOLS=ON \
-DBUILD_CURVE_TOOLS=ON \
- -DHAVE_GMIC=OFF \
-DRAWSPEED_ENABLE_LTO=ON \
- ..
-%else
+ -DUSE_AI=ON \
+ -DONNXRUNTIME_OFFLINE=ON
+%elif (%{defined fedora} && 0%{?fedora} == 43)
%cmake \
-DCMAKE_LIBRARY_PATH:PATH=%{_libdir} \
-DUSE_GEO:BOOLEAN=ON \
@@ -198,25 +157,13 @@ pushd %{_target_platform}
-DRAWSPEED_ENABLE_LTO=ON
%endif
-%if ((%{defined rhel} && 0%{?rhel} > 8) || %{defined fedora})
+
%cmake_build
-%else
-%make_build
-popd
-%endif
%install
-%if %{defined rhel}
-. /opt/rh/gcc-toolset-12/enable
-%endif
-%if ((%{defined rhel} && 0%{?rhel} > 8) || %{defined fedora})
%cmake_install
-%else
-pushd %{_target_platform}
-%make_install
-popd
-%endif
+
%find_lang %{name}
rm -rf %{buildroot}%{_datadir}/doc/darktable
diff --git a/lua.patch b/lua.patch
new file mode 100644
index 0000000..e4cd323
--- /dev/null
+++ b/lua.patch
@@ -0,0 +1,3069 @@
+diff -urNr darktable-5.6.0-orig/cmake/modules/FindLua55.cmake darktable-5.6.0/cmake/modules/FindLua55.cmake
+--- darktable-5.6.0-orig/cmake/modules/FindLua55.cmake 1970-01-01 01:00:00.000000000 +0100
++++ darktable-5.6.0/cmake/modules/FindLua55.cmake 2026-09-01 09:39:54.991134478 +0200
+@@ -0,0 +1,49 @@
++SET(Lua_VERSION_TOO_HIGH 5.6)
++
++include(Prebuilt)
++include(LibFindMacros)
++
++libfind_pkg_search_module(Lua55 lua55 lua5.5 lua-5.5 lua)
++
++if(Lua55_FIND_VERSION)
++ cmake_minimum_required(VERSION 3.10.0)
++ set(Lua55_FAILED_VERSION_CHECK true)
++
++ if(Lua55_FIND_VERSION_EXACT)
++ if(Lua55_VERSION VERSION_EQUAL Lua55_FIND_VERSION)
++ set(Lua55_FAILED_VERSION_CHECK false)
++ endif()
++ else()
++ if(Lua55_VERSION VERSION_EQUAL Lua55_FIND_VERSION OR
++ Lua55_VERSION VERSION_GREATER Lua55_FIND_VERSION AND
++ Lua55_VERSION VERSION_LESS Lua_VERSION_TOO_HIGH)
++ set(Lua55_FAILED_VERSION_CHECK false)
++ endif()
++ endif()
++
++ if(Lua55_FAILED_VERSION_CHECK)
++ if(Lua55_FIND_REQUIRED AND NOT Lua55_FIND_QUIETLY)
++ if(Lua55_FIND_VERSION_EXACT)
++ message(FATAL_ERROR "Lua5.5 version check failed. Version ${Lua55_VERSION} was found, version ${Lua55_FIND_VERSION} is needed exactly.")
++ else(Lua55_FIND_VERSION_EXACT)
++ message(FATAL_ERROR "Lua5.5 version check failed. Version ${Lua55_VERSION} was found, at least version ${Lua55_FIND_VERSION} is required")
++ endif(Lua55_FIND_VERSION_EXACT)
++ endif(Lua55_FIND_REQUIRED AND NOT Lua55_FIND_QUIETLY)
++
++ set(Lua55_FOUND false)
++ endif(Lua55_FAILED_VERSION_CHECK)
++
++endif(Lua55_FIND_VERSION)
++
++if(Lua55_FOUND)
++ set(Lua55 ON CACHE BOOL "Build with lua5.5 support.")
++ if(APPLE)
++ foreach(i ${Lua55_LIBRARIES})
++ find_library(_lua55_LIBRARY NAMES ${i} HINTS ${Lua55_LIBRARY_DIRS})
++ list(APPEND Lua55_LIBRARIES_FULL ${_lua55_LIBRARY})
++ unset(_lua55_LIBRARY CACHE)
++ endforeach(i)
++ set(Lua55_LIBRARIES ${Lua55_LIBRARIES_FULL})
++ unset(Lua55_LIBRARIES_FULL CACHE)
++ endif()
++endif(Lua55_FOUND)
+diff -urNr darktable-5.6.0-orig/src/CMakeLists.txt darktable-5.6.0/src/CMakeLists.txt
+--- darktable-5.6.0-orig/src/CMakeLists.txt 2026-06-21 10:20:23.000000000 +0200
++++ darktable-5.6.0/src/CMakeLists.txt 2026-09-01 09:50:39.335388752 +0200
+@@ -760,14 +760,14 @@
+ # Build external deps
+ #
+ if(USE_LUA)
+- find_package(Lua54 5.4)
+- if(Lua54_FOUND)
++ find_package(Lua55 5.5)
++ if(Lua55_FOUND)
+ message(STATUS "Lua support: Enabled")
+- include_directories(SYSTEM ${Lua54_INCLUDE_DIRS})
+- list(APPEND LIBS ${Lua54_LDFLAGS})
++ include_directories(SYSTEM ${Lua55_INCLUDE_DIRS})
++ list(APPEND LIBS ${Lua55_LDFLAGS})
+ else()
+ if(DONT_USE_INTERNAL_LUA)
+- message(FATAL_ERROR "Lua support: System library for Lua 5.4 not found (to use darktable's version add -DDONT_USE_INTERNAL_LUA=Off)")
++ message(STATUS "Lua support: System library for lua 5.5 not found (to use darktable's version use -DDONT_USE_INTERNAL_LUA=Off)")
+ else(DONT_USE_INTERNAL_LUA)
+ message(STATUS "Lua support: System library not found (using darktable's version)")
+ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/external/lua/src)
+@@ -830,9 +830,9 @@
+ endif(USE_LUA)
+
+ if(USE_LUA)
+- if(NOT Lua54_FOUND)
++ if(NOT Lua55_FOUND)
+ list(APPEND LIB_DEPS lua) # 2nd half of the ugliness :/
+- endif(NOT Lua54_FOUND)
++ endif(NOT Lua55_FOUND)
+ # liblautoc for lua automated interface generation
+ add_library(lautoc_static STATIC IMPORTED)
+ set_target_properties(lautoc_static PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/external/LuaAutoC/liblautoc.a POSITION_INDEPENDENT_CODE True)
+diff -urNr darktable-5.6.0-orig/src/external/lua-scripts/contrib/enfuseAdvanced.lua darktable-5.6.0/src/external/lua-scripts/contrib/enfuseAdvanced.lua
+--- darktable-5.6.0-orig/src/external/lua-scripts/contrib/enfuseAdvanced.lua 2026-06-12 05:06:38.000000000 +0200
++++ darktable-5.6.0/src/external/lua-scripts/contrib/enfuseAdvanced.lua 2026-09-01 09:42:04.370928918 +0200
+@@ -527,9 +527,9 @@
+ local set_tag = GUI.Target.add_tags.text
+ if set_tag ~= nil then --add additional user-specified tags
+ for tag in string.gmatch(set_tag, '[^,]+') do
+- tag = CleanSpaces(tag)
+- tag = dt.tags.create(tag)
+- dt.tags.attach(tag, imported)
++ local cleaned_tag = CleanSpaces(tag)
++ cleaned_tag = dt.tags.create(cleaned_tag)
++ dt.tags.attach(cleaned_tag, imported)
+ end
+ end
+ end
+diff -urNr darktable-5.6.0-orig/src/external/lua-scripts/contrib/enfuseAdvanced.lua.orig darktable-5.6.0/src/external/lua-scripts/contrib/enfuseAdvanced.lua.orig
+--- darktable-5.6.0-orig/src/external/lua-scripts/contrib/enfuseAdvanced.lua.orig 1970-01-01 01:00:00.000000000 +0100
++++ darktable-5.6.0/src/external/lua-scripts/contrib/enfuseAdvanced.lua.orig 2026-06-12 05:06:38.000000000 +0200
+@@ -0,0 +1,1175 @@
++--[[Enfuse Advanced plugin for darktable 2.2.X and 2.4.X
++
++ copyright (c) 2017, 2018 Holger Klemm (Original Linux-only version)
++ Modified by: Kevin Ertel
++
++ darktable 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, either version 3 of the License, or
++ (at your option) any later version.
++
++ darktable 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.
++
++ You should have received a copy of the GNU General Public License
++ along with darktable. If not, see <http://www.gnu.org/licenses/>.
++]]
++
++--[[About this plugin
++This plugin will add the new export module 'fusion to DRI or DFF image'.
++
++----REQUIRED SOFTWARE----
++align_image_stack
++enfuse ver. 4.2 or greater
++exiftool
++
++----USAGE----
++Install:
++ 1) Get the Lua scripts: https://github.com/darktable-org/lua-scripts#download-and-install
++ 2) Require this file in your luarc file, as with any other dt plug-in: require "contrib/enfuseAdvanced"
++ 3) Then select "DRI or DFF image" as storage option
++ 4) On the initial startup set your executable paths
++
++DRI = Dynamic Range Increase (Blend multiple bracket images into a single LDR file)
++DFF = Depth From Focus ('Focus Stacking' - Blend multiple images with different focus into a single image)
++Select multiple images that are either bracketed, or focus-shifted, set your desired operating parameters, and press the export button. A new image will be created. The image will
++be auto imported into darktable if you have that option enabled. Additional tags or style can be applied on auto import as well, if you desire.
++
++image align options:
++See align_image_stack documentation for further explanation of how it specifically works and the options provided (http://hugin.sourceforge.net/docs/manual/Align_image_stack.html)
++
++image fustion options:
++See enfuse documentation for further explanation of how it specifically works and the options provided (https://wiki.panotools.org/Enfuse)
++If you have a specific set of parameters you frequently like to use, you can save them to a preset. There are 3 presets available for DRI, and 3 for DFF.
++
++target file:
++Select your file destination path, or check the 'save to source image location' option.
++'Create Unique Filename' is enabled by default at startup, the user can choose to overwrite existing
++Set any tags or style you desire to be added to the new image (only available if the auto-import option is enabled). You can also change the defaults for this under settings > lua options
++
++format options:
++Same as other export modules
++
++global options:
++Same as other export modules
++
++]]
++
++local dt = require 'darktable'
++local df = require 'lib/dtutils.file'
++local dsys = require 'lib/dtutils.system'
++local du = require 'lib/dtutils'
++local mod = 'module_enfuseAdvanced'
++local os_path_seperator = '/'
++if dt.configuration.running_os == 'windows' then os_path_seperator = '\\' end
++
++du.check_min_api_version("7.0.0", "enfuseAdvanced")
++
++-- Tell gettext where to find the .mo file translating messages for a particular domain
++local gettext = dt.gettext.gettext
++
++local function _(msgid)
++ return gettext(msgid)
++end
++
++-- return data structure for script_manager
++
++local script_data = {}
++
++script_data.metadata = {
++ name = _("enfuse advanced"),
++ purpose = _("focus stack or exposure blend images"),
++ author = "Kevin Ertel",
++ help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/enfuseAdvanced"
++}
++
++script_data.destroy = nil -- function to destory the script
++script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil
++script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again
++script_data.show = nil -- only required for libs since the destroy_method only hides them
++
++-- INITS --
++local AIS = {
++ name = 'align_image_stack',
++ bin = '',
++ first_run = true,
++ install_error = false,
++ arg_string = '',
++ images_string = '',
++ args = {
++ radial_distortion = {text = '-d', style = 'bool'},
++ optimize_field = {text = '-m', style = 'bool'},
++ optimize_image_center = {text = '-i', style = 'bool'},
++ auto_crop = {text = '-C', style = 'bool'},
++ distortion = {text = '--distortion', style = 'bool'},
++ gpu = {text = '--gpu', style = 'bool'},
++ grid_size = {text = '-g ', style = 'integer'},
++ control_points = {text = '-c ', style = 'integer'},
++ control_points_remove = {text = '-t ', style = 'integer'},
++ correlation = {text = '--corr=', style = 'float'}
++ }
++}
++local ENF = {
++ name = 'enfuse',
++ bin = '',
++ first_run = true,
++ install_error = false,
++ arg_string = '',
++ image_string = '',
++ args = {
++ exposure_weight = {text = '--exposure-weight=', style = 'float'},
++ saturation_weight = {text = '--saturation-weight=', style = 'float'},
++ contrast_weight = {text = '--contrast-weight=', style = 'float'},
++ exposure_optimum = {text = '--exposure-optimum=', style = 'float'},
++ exposure_width = {text = '--exposure-width=', style = 'float'},
++ hard_masks = {text = '--hard-mask', style = 'bool'},
++ save_masks = {text = '--save-masks', style = 'bool'},
++ contrast_window_size = {text = '--contrast-window-size=', style = 'integer'},
++ contrast_edge_scale = {text = '--contrast-edge-scale=', style = 'float'},
++ contrast_min_curvature = {text = '--contrast-min-curvature=', style = 'string'}
++ }
++}
++local EXF = {
++ name = 'exiftool',
++ bin = '',
++ first_run = true,
++ install_error = false
++}
++local GUI = {
++ AIS = {
++ radial_distortion = {},
++ optimize_field = {},
++ optimize_image_center = {},
++ auto_crop = {},
++ distortion = {},
++ grid_size = {},
++ control_points = {},
++ control_points_remove = {},
++ correlation = {}},
++ ENF = {
++ exposure_weight = {},
++ saturation_weight = {},
++ contrast_weight = {},
++ exposure_optimum = {},
++ exposure_width = {},
++ hard_masks = {},
++ save_masks = {},
++ contrast_window_size = {},
++ contrast_edge_scale = {},
++ contrast_min_curvature = {}},
++ Target = {
++ format = {},
++ depth = {},
++ compression_level_tif = {},
++ compression_level_jpg = {},
++ output_compress = {},
++ output_directory = {},
++ source_location = {},
++ on_conflict = {},
++ auto_import = {},
++ apply_style = {},
++ copy_tags = {},
++ add_tags = {}},
++ Presets = {
++ current_preset ={};
++ load ={};
++ save ={};
++ variants ={};
++ variants_type ={}},
++ exes = {
++ align_image_stack = {},
++ enfuse = {},
++ exiftool = {},
++ update = {}
++ },
++ align = {},
++ options_contain = {},
++ show_options = {}
++}
++
++local styles = dt.styles
++local styles_count = 1 -- 'none' = 1
++for _,i in pairs(dt.styles) do
++ if type(i) == 'userdata' then styles_count = styles_count + 1 end
++end
++
++-- FUNCTION --
++
++local function sanitize_decimals(cmd) -- make sure decimal separator is a '.'
++ return string.gsub(cmd, '(%d),(%d)', "%1.%2")
++end
++
++local function InRange(test, low, high) -- tests if test value is within range of low and high (inclusive)
++ if test >= low and test <= high then
++ return true
++ else
++ return false
++ end
++end
++
++local function GetFileName(full_path) -- Parses a full path (path/filename_identifier.extension) into individual parts
++--[[Input: Folder1/Folder2/Folder3/Img_0001.CR2
++
++ Returns:
++ path: Folder1/Folder2/Folder3/
++ filename: Img_0001
++ identifier: 0001
++ extension: .CR2
++
++ EX:
++ path_1, file_1, id_1, ext_1 = GetFileName(full_path_1)
++ ]]
++ local path = string.match(full_path, '.*[\\/]')
++ local filename = string.gsub(string.match(full_path, '[%w-_]*%.') , '%.' , '' )
++ local identifier = string.match(filename, '%d*$')
++ local extension = string.match(full_path, '%.%w*')
++ return path, filename, identifier, extension
++end
++
++local function CleanSpaces(text) --removes spaces from the front and back of passed in text
++ text = string.gsub(text,'^%s*','')
++ text = string.gsub(text,'%s*$','')
++ return text
++end
++
++local function BuildExecuteCmd(prog_table) --creates a program command using elements of the passed in program table
++ local result = CleanSpaces(prog_table.bin)..' '..CleanSpaces(prog_table.arg_string)..' '..CleanSpaces(prog_table.images_string)
++ return result
++end
++
++local function PreCall(prog_tbls) --looks to see if this is the first call, if so checks to see if program is installed properly
++ for _,prog in pairs(prog_tbls) do
++ if prog.first_run then
++ prog.bin = df.check_if_bin_exists(prog.name)
++ if not prog.bin then
++ prog.install_error = true
++ dt.preferences.write(mod, 'bin_exists', 'bool', false)
++ else
++ prog.bin = CleanSpaces(prog.bin)
++ end
++ prog.first_run = false
++ end
++ end
++ if not dt.preferences.read(mod, 'bin_exists', 'bool') then
++ GUI.options_contain.active = 4
++ GUI.show_options.sensitive = false
++ dt.print(_('please update your binary locations'))
++ end
++end
++
++local function ExeUpdate(prog_tbl) --updates executable paths and verifies them
++ dt.preferences.write(mod, 'bin_exists', 'bool', true)
++ for x,prog in pairs(prog_tbl) do
++ dt.preferences.write('executable_paths', prog.name, 'string', GUI.exes[prog.name].value)
++ prog.bin = df.check_if_bin_exists(prog.name)
++ if not prog.bin then
++ prog.install_error = true
++ dt.preferences.write(mod, 'bin_exists', 'bool', false)
++ dt.print(string.format(_("issue with %s executable"), prog.name))
++ else
++ prog.bin = CleanSpaces(prog.bin)
++ end
++ prog.first_run = false
++ end
++ if dt.preferences.read(mod, 'bin_exists', 'bool') then
++ GUI.options_contain.active = 2
++ GUI.show_options.sensitive = true
++ dt.print(_('update successful'))
++ else
++ dt.print(_('update unsuccessful, please try again'))
++ end
++end
++
++local function GetArgsFromPreference(prog_table, prefix) --for each arg in a program table reads in the associated value in the active preference (which is continually updated via clicked/changed callback in GUI elements
++ prog_table.arg_string = ''
++ for argument, arg_data in pairs(prog_table.args) do
++ local temp = dt.preferences.read(mod, prefix..argument, arg_data.style)
++ if arg_data.style == 'bool' and temp then
++ prog_table.arg_string = prog_table.arg_string..arg_data.text..' '
++ elseif arg_data.style == 'integer' then
++ prog_table.arg_string = prog_table.arg_string..arg_data.text..temp..' '
++ elseif arg_data.style == 'string' then
++ if dt.configuration.running_os == "windows" then
++ --escape % with %% as % means env var on windows and call will fail otherwise
++ temp = string.gsub(temp, "%%", "%%%%")
++ end
++ prog_table.arg_string = prog_table.arg_string..arg_data.text..temp..' '
++ elseif arg_data.style == 'float' then
++ temp = string.sub(tostring(temp),1,3)
++ prog_table.arg_string = prog_table.arg_string..arg_data.text..temp..' '
++ end
++ end
++ prog_table.arg_string = CleanSpaces(prog_table.arg_string)
++ return prog_table.arg_string
++end
++
++local function UpdateAISargs(image_table, images_to_remove) --updates the AIS arguments, builds the input image string, returns a modified image table which contains the aligned image names in place of the exported image names also updates the images to remove string with new aligned images and a string of the images to align
++ GetArgsFromPreference(AIS, 'active_')
++ local source_path = ''
++ local images_to_align = ''
++ local index = 0
++ for raw, temp_image in pairs(image_table) do
++ local index_str = ''
++ source_path = GetFileName(temp_image)
++ images_to_align = images_to_align..df.sanitize_filename(temp_image)..' '
++ if InRange(index,0,9) then index_str = '000'..tostring(index)
++ elseif InRange(index,10,99) then index_str = '00'..tostring(index)
++ end
++ image_table[raw] = source_path..'aligned_'..index_str..'.tif'
++ images_to_remove = images_to_remove..df.sanitize_filename(image_table[raw])..' '
++ index = index + 1
++ end
++ source_path = df.sanitize_filename(source_path..'aligned_')
++ AIS.arg_string = AIS.arg_string..' -a '..source_path
++ images_to_align = CleanSpaces(images_to_align)
++
++ return image_table, images_to_remove, images_to_align
++end
++
++local function UpdateENFargs(image_table, prefix) --updates the Enfuse arguments, builds the input image string, generates output filename, returns string of images to blend, the name of the outout image, and the first raw image object
++ GetArgsFromPreference(ENF, prefix)
++ local images_to_blend = ''
++ local out_path = ''
++ local out_name = ''
++ local smallest_name = ''
++ local smallest_id = math.huge
++ local largest_id = 0
++ local first_raw = {}
++ for raw, temp_image in pairs(image_table) do
++ local _, source_name, source_id = GetFileName(raw.filename)
++ source_id = tonumber(source_id)
++ if source_id < smallest_id then
++ smallest_id = source_id
++ smallest_name = source_name
++ first_raw = raw
++ end
++ if source_id > largest_id then largest_id = source_id end
++ out_path = raw.path
++ images_to_blend = images_to_blend..df.sanitize_filename(temp_image)..' '
++ end
++ ENF.arg_string = ENF.arg_string..' --depth='..GUI.Target.depth.value..' '
++ if GUI.Target.format.value == 'tif' then ENF.arg_string = ENF.arg_string..'--compression='..GUI.Target.compression_level_tif.value..' '
++ elseif GUI.Target.format.value == 'jpg' then ENF.arg_string = ENF.arg_string..'--compression='..tostring(math.floor(GUI.Target.compression_level_jpg.value))..' '
++ end
++ if not GUI.Target.source_location.value then out_path = GUI.Target.output_directory.value end
++ out_name = smallest_name..'-'..largest_id
++ out_path = out_path..os_path_seperator..out_name..'.'..GUI.Target.format.value
++ if GUI.Target.on_conflict.value == 'create unique filename' then out_path = df.create_unique_filename(out_path) end
++ ENF.arg_string = ENF.arg_string..'--output='..df.sanitize_filename(out_path)
++ images_to_blend = CleanSpaces(images_to_blend)
++
++ return images_to_blend, out_path, first_raw
++end
++
++local function UpdateActivePreference() --sliders & entry boxes do not have a click/changed callback, so their values must be saved to the active preference 'manually'
++ local enf = {'exposure_weight','saturation_weight','contrast_weight','exposure_optimum','exposure_width'}
++ for _,descriptor in pairs(enf) do
++ temp = GUI.ENF[descriptor].value
++ dt.preferences.write(mod, 'active_'..descriptor, 'float', temp)
++ end
++ temp = GUI.Target.compression_level_jpg.value
++ temp = math.floor(temp)
++ dt.preferences.write(mod, 'active_compression_level_jpg', 'integer', temp)
++ temp = GUI.Target.add_tags.text
++ dt.preferences.write(mod, 'active_add_tags', 'string', temp)
++end
++
++local function SaveToPreference(preset) --save the present values of enfuse GUI elements to the specified 'preset'
++ UpdateActivePreference()
++ for argument, arg_data in pairs(ENF.args) do
++ local temp
++ if argument == 'contrast_window_size' or argument == 'contrast_edge_scale' or argument == 'contrast_min_curvature' then --comboboxes must be handled specially via an index value
++ temp = dt.preferences.read(mod, 'active_'..argument..'_ind', 'integer')
++ dt.preferences.write(mod, preset..argument..'_ind', 'integer', temp)
++ temp = dt.preferences.read(mod, 'active_'..argument, arg_data.style)
++ dt.preferences.write(mod, preset..argument, arg_data.style, temp)
++ else
++ temp = dt.preferences.read(mod, 'active_'..argument, arg_data.style)
++ dt.preferences.write(mod, preset..argument, arg_data.style, temp)
++ end
++ end
++ dt.print(string.format(_("saved to %s"), preset))
++end
++
++local function LoadFromPreference(preset) --load values from the specified 'preset' into the GUI elements
++ for argument, arg_data in pairs(ENF.args) do
++ local temp
++ if argument == 'contrast_window_size' or argument == 'contrast_edge_scale' or argument == 'contrast_min_curvature' then --comboboxes must be handled specially via an index value
++ temp = dt.preferences.read(mod, preset..argument..'_ind', 'integer')
++ GUI.ENF[argument].selected = temp
++ dt.preferences.write(mod, 'active_'..argument..'_ind', 'integer', temp)
++ else
++ temp = dt.preferences.read(mod, preset..argument, arg_data.style)
++ GUI.ENF[argument].value = temp
++ dt.preferences.write(mod, 'active_'..argument, arg_data.style, temp)
++ end
++ end
++ dt.print(string.format(_("loaded from %s"), preset))
++end
++
++local function remove_temp_files(images_to_remove) --deletes all files specified by the input string
++ if dt.configuration.running_os == 'windows' then
++ dt.control.execute('del '..images_to_remove)
++ else
++ dt.control.execute('rm '..images_to_remove)
++ end
++end
++
++local function initial(storage, format, images, high_quality, extra_data) --called before export happens, ensure enough images selected and all necesary programs installed correctly, if error then it sets a bit in extra_data for use by main and cancels export
++ if #images <2 then
++ table.insert(extra_data, 1, 1)
++ return {}
++ else
++ PreCall({ENF,AIS,EXF})
++ if AIS.install_error or ENF.install_error or EXF.install_error then
++ table.insert(extra_data, 1, 2)
++ return {}
++ else
++ table.insert(extra_data, 1, 0)
++ return images
++ end
++ end
++end
++
++local function support_format(storage, format) --tells dt we only support TIFF export type
++ local ret = false
++ local temp = string.find(format.name, 'TIFF')
++ if temp ~= nil then ret = true end
++ return ret
++end
++
++local function show_status(storage, image, format, filename, number, total, high_quality, extra_data) --outputs message to user showing script export status
++ dt.print(string.format(_("export for image fusion %d / %d"), math.floor(number), math.floor(total)))
++end
++
++local function main(storage, image_table, extra_data)
++ if extra_data[1] == 1 then
++ dt.print(_('too few images selected, please select at least 2 images'))
++ return
++ elseif extra_data[1] == 2 then
++ dt.print(_('installation error, please verify binary paths are correct'))
++ return
++ end
++ local images_to_remove = ''
++ local final_image = nil
++ local source_raw = nil
++ for raw,exported in pairs(image_table) do --add the exported files to list of images to remove when complete\fail
++ images_to_remove = images_to_remove..df.sanitize_filename(exported)..' '
++ end
++ if GUI.align.value then --if user selected to align images then update AIS arguments and execute AIS command
++ local job = dt.gui.create_job('aligning images')
++ image_table, images_to_remove, AIS.images_string = UpdateAISargs(image_table, images_to_remove)
++ local run_cmd = BuildExecuteCmd(AIS)
++ dt.print_log("AIS run command is " .. run_cmd)
++ run_cmd = sanitize_decimals(run_cmd)
++ dt.print_log("AIS decimaal sanitized command is " .. run_cmd)
++ local resp = dsys.external_command(run_cmd)
++ job.valid = false
++ if resp ~= 0 then
++ remove_temp_files(images_to_remove)
++ dt.print(string.format(_("%s failed"), AIS.name))
++ dt.print_error(AIS.name .. ' failed')
++ return
++ end
++ end
++ variants = {'active_'} --default to 'active' settings unless user selected to create multiple image variants from the specified preset types
++ if GUI.Presets.variants.value then
++ if GUI.Presets.variants_type.value == 'dri' then
++ variants = {'dri_1', 'dri_2', 'dri_3'}
++ else
++ variants = {'dff_2', 'dff_2', 'dff_3'}
++ end
++ end
++ local image_num = 0
++ local job = dt.gui.create_job('blending '..#variants..' image(s)', true) --create a GUI job bar to display enfuse progress
++ UpdateActivePreference() --load current GUI values into active preference (only applies to elements without a clicked/changed callback)
++ for x,prefix in pairs(variants) do --for each image to be created load in the preference values, build arguments string, output image, and run command then execute.
++ job.percent = image_num /(#variants)
++ image_num = image_num+1
++ ENF.images_string, final_image, source_raw = UpdateENFargs(image_table, prefix)
++ local run_cmd = BuildExecuteCmd(ENF)
++ dt.print_log("ENF run command is " .. run_cmd)
++ run_cmd = sanitize_decimals(run_cmd)
++ dt.print_log("ENF decimaal sanitized command is " .. run_cmd)
++ local resp = dsys.external_command(run_cmd)
++ if resp ~= 0 then
++ remove_temp_files(images_to_remove)
++ dt.print_error(ENF.name..' failed')
++ dt.print(string.format(_("%s failed"), ENF.name))
++ job.valid = false
++ return
++ end
++
++ --copy exif data from original file
++ run_cmd = EXF.bin..' -TagsFromFile '..df.sanitize_filename(source_raw.path..os_path_seperator..source_raw.filename)..' -exif:all --subifd:all -overwrite_original '..df.sanitize_filename(final_image)
++ -- replace comma decimal separator with period
++ dt.print_log("EXF run command is " .. run_cmd)
++ run_cmd = sanitize_decimals(run_cmd)
++ dt.print_log("EXF decimaal sanitized command is " .. run_cmd)
++ resp = dsys.external_command(run_cmd)
++
++
++ if GUI.Target.auto_import.value then --import image into dt if specified
++ local imported = dt.database.import(final_image)
++ dt.print_log("image imported")
++ if GUI.Target.apply_style.selected > 1 then --apply specified style to imported image
++ local set_style = styles[GUI.Target.apply_style.selected - 1]
++ dt.styles.apply(set_style , imported)
++ end
++ if GUI.Target.copy_tags.value then --copy tags from source image
++ local all_tags = dt.tags.get_tags(source_raw)
++ for _,tag in pairs(all_tags) do
++ if string.match(tag.name, 'darktable|') == nil then dt.tags.attach(tag, imported) end
++ end
++ end
++ local set_tag = GUI.Target.add_tags.text
++ if set_tag ~= nil then --add additional user-specified tags
++ for tag in string.gmatch(set_tag, '[^,]+') do
++ tag = CleanSpaces(tag)
++ tag = dt.tags.create(tag)
++ dt.tags.attach(tag, imported)
++ end
++ end
++ end
++ end
++ remove_temp_files(images_to_remove)
++ job.valid = false
++ dt.print('image fusion process complete')
++end
++
++local function destroy()
++ dt.destroy_storage('module_enfuseAdvanced')
++end
++
++--GUI--
++stack_compression = dt.new_widget('stack'){}
++local label_AIS_options= dt.new_widget('section_label'){
++ label = _('image align options')
++}
++GUI.align = dt.new_widget('check_button'){
++ label = _('align images'),
++ value = dt.preferences.read(mod, 'active_align', 'bool'),
++ tooltip = _('automatically align images prior to enfuse'),
++ clicked_callback = function(self)
++ dt.preferences.write(mod, 'active_align', 'bool', self.value)
++ for _,widget in pairs(GUI.AIS) do
++ widget.sensitive = self.value
++ end
++ end,
++ reset_callback = function(self) self.value = true end
++}
++GUI.AIS.radial_distortion = dt.new_widget('check_button'){
++ label = _('optimize radial distortion'),
++ value = dt.preferences.read(mod, 'active_radial_distortion', 'bool'),
++ tooltip = _('optimize radial distortion for all images, \nexcept for first'),
++ clicked_callback = function(self) dt.preferences.write(mod, 'active_radial_distortion', 'bool', self.value) end,
++ reset_callback = function(self) self.value = true end
++}
++GUI.AIS.optimize_field = dt.new_widget('check_button'){
++ label = _('optimize field of view'),
++ value = dt.preferences.read(mod, 'active_optimize_field', 'bool'),
++ tooltip = _('optimize field of view for all images, except for first. \nUseful for aligning focus stacks (DFF) with slightly \ndifferent magnification.'),
++ clicked_callback = function(self) dt.preferences.write(mod, 'active_optimize_field', 'bool', self.value) end,
++ reset_callback = function(self) self.value = true end
++}
++GUI.AIS.optimize_image_center = dt.new_widget('check_button'){
++ label = _('optimize image center shift'),
++ value = dt.preferences.read(mod, 'active_optimize_image_center', 'bool'),
++ tooltip = _('optimize image center shift for all images, \nexcept for first.'),
++ clicked_callback = function(self) dt.preferences.write(mod, 'active_optimize_image_center', 'bool', self.value) end,
++ reset_callback = function(self) self.value = true end
++}
++GUI.AIS.auto_crop = dt.new_widget('check_button'){
++ label = _('auto crop'),
++ value = dt.preferences.read(mod, 'active_auto_crop', 'bool'),
++ tooltip = _('auto crop the image to the area covered by all images.'),
++ clicked_callback = function(self) dt.preferences.write(mod, 'active_auto_crop', 'bool', self.value) end,
++ reset_callback = function(self) self.value = true end
++}
++GUI.AIS.distortion = dt.new_widget('check_button'){
++ label = _('load distortion from lens database'),
++ value = dt.preferences.read(mod, 'active_distortion', 'bool'),
++ tooltip = _('try to load distortion information from lens database'),
++ clicked_callback = function(self) dt.preferences.write(mod, 'active_distortion', 'bool', self.value) end,
++ reset_callback = function(self) self.value = true end
++}
++GUI.AIS.gpu = dt.new_widget('check_button'){
++ label = _('use gpu'),
++ value = dt.preferences.read(mod, 'active_gpu', 'bool'),
++ tooltip = _('use gpu during alignment'),
++ clicked_callback = function(self) dt.preferences.write(mod, 'active_gpu', 'bool', self.value) end,
++ reset_callback = function(self) self.value = true end
++}
++temp = dt.preferences.read(mod, 'active_grid_size_ind', 'integer')
++if not InRange(temp, 1, 9) then temp = 5 end
++GUI.AIS.grid_size = dt.new_widget('combobox'){
++ label = _('image grid size'),
++ tooltip = _('break image into a rectangular grid \nand attempt to find num control points in each section.\ndefault: (5x5)'),
++ selected = temp,
++ '1','2','3','4','5','6','7','8','9',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_grid_size', 'integer', self.value)
++ dt.preferences.write(mod, 'active_grid_size_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 5
++ dt.preferences.write(mod, 'active_grid_size', 'integer', self.value)
++ dt.preferences.write(mod, 'active_grid_size_ind', 'integer', self.selected)
++ end
++}
++temp = dt.preferences.read(mod, 'active_control_points_ind', 'integer')
++if not InRange(temp, 1, 9) then temp = 8 end
++GUI.AIS.control_points = dt.new_widget('combobox'){
++ label = _('control points/grid'),
++ tooltip = _('number of control points (per grid, see option -g) \nto create between adjacent images \ndefault: (8).'),
++ selected = temp,
++ '1','2','3','4','5','6','7','8','9',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_control_points', 'integer', self.value)
++ dt.preferences.write(mod, 'active_control_points_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 8
++ dt.preferences.write(mod, 'active_control_points', 'integer', self.value)
++ dt.preferences.write(mod, 'active_control_points_ind', 'integer', self.selected)
++ end
++}
++temp = dt.preferences.read(mod, 'active_control_points_remove_ind', 'integer')
++if not InRange(temp, 1, 9) then temp = 3 end
++GUI.AIS.control_points_remove = dt.new_widget('combobox'){
++ label = _('remove control points with error'),
++ tooltip = _('remove all control points with an error higher \nthan num pixels \ndefault: (3)'),
++ selected = temp,
++ '1','2','3','4','5','6','7','8','9',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_control_points_remove', 'integer', self.value)
++ dt.preferences.write(mod, 'active_control_points_remove_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 3
++ dt.preferences.write(mod, 'active_control_points_remove', 'integer', self.value)
++ dt.preferences.write(mod, 'active_control_points_remove_ind', 'integer', self.selected)
++ end
++}
++temp = dt.preferences.read(mod, 'active_correlation_ind', 'integer')
++if not InRange(temp, 1, 10) then temp = 9 end
++GUI.AIS.correlation = dt.new_widget('combobox'){
++ label = _('correlation threshold for control points'),
++ tooltip = _('correlation threshold for identifying \ncontrol points \ndefault: (0.9).'),
++ selected = temp,
++ '0.1','0.2','0.3','0.4','0.5','0.6','0.7','0.8','0.9','1.0',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_correlation', 'float', self.value)
++ dt.preferences.write(mod, 'active_correlation_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 9
++ dt.preferences.write(mod, 'active_correlation', 'float', self.value)
++ dt.preferences.write(mod, 'active_correlation_ind', 'integer', self.selected)
++ end
++}
++local label_ENF_options= dt.new_widget('section_label'){
++ label = _('image fusion options')
++}
++temp = dt.preferences.read(mod, 'active_exposure_weight', 'float')
++if not InRange(temp, 0, 1) then temp = 1 end
++GUI.ENF.exposure_weight = dt.new_widget('slider'){
++ label = _('exposure weight'),
++ tooltip = _('set the relative weight of the well-exposedness criterion \nas defined by the chosen exposure weight function. \nincreasing this weight relative to the others will\n make well-exposed pixels contribute more to\n the final output. \ndefault: (1.0)'),
++ hard_min = 0,
++ hard_max = 1,
++ value = temp,
++ reset_callback = function(self)
++ self.value = 1
++ end
++}
++temp = dt.preferences.read(mod, 'active_saturation_weight', 'float')
++if not InRange(temp, 0, 1) then temp = .2 end
++GUI.ENF.saturation_weight = dt.new_widget('slider'){
++ label = _('saturation weight'),
++ tooltip = _('set the relative weight of high-saturation pixels. \nincreasing this weight makes pixels with high \nsaturation contribute more to the final output. \ndefault: (0.2)'),
++ hard_min = 0,
++ hard_max = 1,
++ value = temp,
++ reset_callback = function(self)
++ self.value = 0.2
++ end
++}
++temp = dt.preferences.read(mod, 'active_contrast_weight', 'float')
++if not InRange(temp, 0, 1) then temp = 0 end
++GUI.ENF.contrast_weight = dt.new_widget('slider'){
++ label = _('contrast weight'),
++ tooltip = _('sets the relative weight of high local-contrast pixels. \ndefault: (0.0).'),
++ hard_min = 0,
++ hard_max = 1,
++ value = temp,
++ reset_callback = function(self)
++ self.value = 0
++ end
++}
++temp = dt.preferences.read(mod, 'active_exposure_optimum', 'float')
++if not InRange(temp, 0, 1) then temp = 0.5 end
++GUI.ENF.exposure_optimum = dt.new_widget('slider'){
++ label = _('exposure optimum'),
++ tooltip = _('determine at what normalized exposure value\n the optimum exposure of the input images\n is. this is, set the position of the maximum\n of the exposure weight curve. use this \noption to fine-tune exposure weighting. \ndefault: (0.5)'),
++ hard_min = 0,
++ hard_max = 1,
++ value = temp,
++ reset_callback = function(self)
++ self.value = 0.5
++ end
++}
++temp = dt.preferences.read(mod, 'active_exposure_width', 'float')
++if not InRange(temp, 0, 1) then temp = 0.2 end
++GUI.ENF.exposure_width = dt.new_widget('slider'){
++ label = _('exposure width'),
++ tooltip = _('set the characteristic width (FWHM) of the exposure \nweight function. low numbers give less weight to \npixels that are far from the user-defined \noptimum and vice versa. use this option to \nfine-tune exposure weighting. \ndefault: (0.2)'),
++ hard_min = 0,
++ hard_max = 1,
++ value = temp,
++ reset_callback = function(self)
++ self.value = 0.2
++ end
++}
++GUI.ENF.hard_masks = dt.new_widget('check_button'){
++ label = _('hard mask'),
++ value = dt.preferences.read(mod, 'active_hard_masks', 'bool'),
++ tooltip = _('force hard blend masks on the finest scale. this avoids \naveraging of fine details (only), at the expense \nof increasing the noise. this improves the \nsharpness of focus stacks considerably.\ndefault (soft mask)'),
++ clicked_callback = function(self) dt.preferences.write(mod, 'active_hard_masks', 'bool', self.value) end,
++ reset_callback = function(self) self.value = false end
++}
++GUI.ENF.save_masks = dt.new_widget('check_button'){
++ label = _('save masks'),
++ value = dt.preferences.read(mod, 'active_save_masks', 'bool'),
++ tooltip = _('save the generated weight masks to your home directory,\nenblend saves masks as 8 bit grayscale, \ni.e. single channel images. \nfor accuracy we recommend to choose a lossless format.'),
++ clicked_callback = function(self) dt.preferences.write(mod, 'active_save_masks', 'bool', self.value) end,
++ reset_callback = function(self) self.value = false end
++}
++temp = dt.preferences.read(mod, 'active_contrast_window_size_ind', 'integer')
++if not InRange(temp, 1, 8) then temp = 3 end
++GUI.ENF.contrast_window_size = dt.new_widget('combobox'){
++ label = _('contrast window size'),
++ tooltip = _('set the window size for local contrast analysis. \nthe window will be a square of size × size pixels. \nif given an even size, Enfuse will \nautomatically use the next odd number.\nfor contrast analysis size values larger \nthan 5 pixels might result in a \nblurry composite image. values of 3 and \n5 pixels have given good results on \nfocus stacks. \ndefault: (5) pixels'),
++ selected = temp,
++ '3','4','5','6','7','8','9','10',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_contrast_window_size', 'integer', self.value)
++ dt.preferences.write(mod, 'active_contrast_window_size_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 3
++ dt.preferences.write(mod, 'active_contrast_window_size', 'integer', self.value)
++ dt.preferences.write(mod, 'active_contrast_window_size_ind', 'integer', self.selected)
++ end
++}
++temp = dt.preferences.read(mod, 'active_contrast_edge_scale_ind', 'integer')
++if not InRange(temp, 1, 6) then temp = 1 end
++GUI.ENF.contrast_edge_scale = dt.new_widget('combobox'){
++ label = _('contrast edge scale'),
++ tooltip = _('a non-zero value for EDGE-SCALE switches on the \nLaplacian-of-Gaussian (LoG) edge detection algorithm.\n edage-scale is the radius of the Gaussian used \nin the search for edges. a positive LCE-SCALE \nturns on local contrast enhancement (LCE) \nbefore the LoG edge detection. \nDefault: (0.0) pixels.'),
++ selected = temp,
++ '0.0','0.1','0.2','0.3','0.4','0.5',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_contrast_edge_scale', 'float', self.value)
++ dt.preferences.write(mod, 'active_contrast_edge_scale_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 1
++ dt.preferences.write(mod, 'active_contrast_edge_scale', 'float', self.value)
++ dt.preferences.write(mod, 'active_contrast_edge_scale_ind', 'integer', self.selected)
++ end
++}
++temp = dt.preferences.read(mod, 'active_contrast_min_curvature_ind', 'integer')
++if not InRange(temp, 1, 11) then temp = 1 end
++GUI.ENF.contrast_min_curvature = dt.new_widget('combobox'){
++ label = _('contrast min curvature [%]'),
++ tooltip = _('define the minimum curvature for the LoG edge detection. Append a ‘%’ to specify the minimum curvature relative to maximum pixel value in the source image. Default: (0.0%)'),
++ selected = temp,
++ '0.0%','0.1%','0.2%','0.3%','0.4%','0.5%','0.6%','0.7%','0.8%','0.9%','1.0%',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_contrast_min_curvature', 'string', self.value)
++ dt.preferences.write(mod, 'active_contrast_min_curvature_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 1
++ dt.preferences.write(mod, 'active_contrast_min_curvature', 'string', self.value)
++ dt.preferences.write(mod, 'active_contrast_min_curvature_ind', 'integer', self.selected)
++ end
++}
++local label_target_options= dt.new_widget('section_label'){
++ label = _('target file')
++}
++temp = dt.preferences.read(mod, 'active_compression_level_tif_ind', 'integer')
++if not InRange(temp, 1, 4) then temp = 1 end
++GUI.Target.compression_level_tif = dt.new_widget('combobox'){
++ label = _('tiff compression'),
++ tooltip = _('compression method for tiff files'),
++ selected = temp,
++ 'none','deflate','lzw','packbits',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_compression_level_tif', 'string', self.value)
++ dt.preferences.write(mod, 'active_compression_level_tif_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 1
++ dt.preferences.write(mod, 'active_compression_level_tif', 'string', self.value)
++ dt.preferences.write(mod, 'active_compression_level_tif_ind', 'integer', self.selected)
++ end
++}
++temp = dt.preferences.read(mod, 'active_compression_level_jpg', 'integer')
++if not InRange(temp, 0, 100) then temp = 0 end
++GUI.Target.compression_level_jpg = dt.new_widget('slider'){
++ label = _('jpeg compression'),
++ tooltip = _('jpeg compression level'),
++ soft_min = 0,
++ soft_max = 100,
++ hard_min = 0,
++ hard_max = 100,
++ step = 5,
++ digits = 0,
++ value = temp,
++ reset_callback = function(self)
++ self.value = 0
++ end
++}
++local blank = dt.new_widget('box'){}
++stack_compression = dt.new_widget('stack'){
++ GUI.Target.compression_level_tif,
++ GUI.Target.compression_level_jpg,
++ blank,
++ active = 1
++}
++temp = dt.preferences.read(mod, 'active_format_ind', 'integer')
++if not InRange(temp, 1, 6) then temp = 1 end
++GUI.Target.format = dt.new_widget('combobox'){
++ label = _('file format'),
++ tooltip = _('file format of the enfused final image'),
++ selected = temp,
++ 'tif','jpg','png','pnm','pbm','ppm',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_format', 'string', self.value)
++ dt.preferences.write(mod, 'active_format_ind', 'integer', self.selected)
++ if self.value == 'tif' then stack_compression.active = 1
++ elseif self.value == 'jpg' then stack_compression.active = 2
++ else stack_compression.active = 3
++ end
++ end,
++ reset_callback = function(self)
++ self.selected = 1
++ dt.preferences.write(mod, 'active_format', 'string', self.value)
++ dt.preferences.write(mod, 'active_format_ind', 'integer', self.selected)
++ end
++}
++temp = dt.preferences.read(mod, 'active_depth_ind', 'integer')
++if not InRange(temp, 1, 5) then temp = 3 end
++GUI.Target.depth = dt.new_widget('combobox'){
++ label = _('bit depth'),
++ tooltip = _('bit depth of the enfused file'),
++ selected = temp,
++ '8','16','32','r32','r64',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_depth', 'string', self.value)
++ dt.preferences.write(mod, 'active_depth_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 2
++ dt.preferences.write(mod, 'active_depth', 'string', self.value)
++ dt.preferences.write(mod, 'active_depth_ind', 'integer', self.selected)
++ end
++}
++local label_directory = dt.new_widget('label'){
++ label = _('directory'),
++ ellipsize = 'start',
++ halign = 'start'
++}
++temp = dt.preferences.read(mod, 'active_output_directory', 'string')
++if temp == '' or temp == nil then temp = dt.collection[1].path end
++GUI.Target.output_directory = dt.new_widget('file_chooser_button'){
++ title = 'Select export path',
++ is_directory = true,
++ tooltip = _('select the target directory for the fused image. \nthe filename is created automatically.'),
++ value = temp,
++ changed_callback = function(self) dt.preferences.write(mod, 'active_output_directory', 'string', self.value) end
++}
++GUI.Target.source_location = dt.new_widget('check_button'){
++ label = _('save to source image location'),
++ value = dt.preferences.read(mod, 'active_source_location', 'bool'),
++ tooltip = _('if checked ignores the location above and saves output image(s) to the same location as the source images.'),
++ clicked_callback = function(self) dt.preferences.write(mod, 'active_source_location', 'bool', self.value) end,
++ reset_callback = function(self) self.value = true end
++}
++temp = dt.preferences.read(mod, 'active_on_conflict_ind', 'integer')
++if not InRange(temp, 1, 2) then temp = 1 end
++GUI.Target.on_conflict = dt.new_widget('combobox'){
++ label = _('on conflict'),
++ selected = 1,
++ 'create unique filename','overwrite',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_on_conflict', 'string', self.value)
++ dt.preferences.write(mod, 'active_on_conflict_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 1
++ dt.preferences.write(mod, 'active_on_conflict', 'string', self.value)
++ dt.preferences.write(mod, 'active_on_conflict_ind', 'integer', self.selected)
++ end
++}
++GUI.Target.auto_import = dt.new_widget('check_button'){
++ label = _('auto import'),
++ value = dt.preferences.read(mod, 'active_auto_import', 'bool'),
++ tooltip = _('import the image into darktable database when enfuse completes'),
++ clicked_callback = function(self)
++ dt.preferences.write(mod, 'active_auto_import', 'bool', self.value)
++ GUI.Target.apply_style.sensitive = self.value
++ GUI.Target.copy_tags.sensitive = self.value
++ GUI.Target.add_tags.sensitive = self.value
++ end,
++ reset_callback = function(self) self.value = true end
++}
++temp = dt.preferences.read(mod, 'active_apply_style_ind', 'integer')
++GUI.Target.apply_style = dt.new_widget('combobox'){
++ label = _('apply style on Import'),
++ tooltip = _('apply selected style on auto-import to newly created blended image'),
++ selected = 1,
++ 'none',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_apply_style', 'string', self.value)
++ dt.preferences.write(mod, 'active_apply_style_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 1
++ dt.preferences.write(mod, 'active_apply_style', 'string', self.value)
++ dt.preferences.write(mod, 'active_apply_style_ind', 'integer', self.selected)
++ end
++}
++for k=1, (styles_count-1) do
++ GUI.Target.apply_style[k+1] = styles[k].name
++end
++if not InRange(temp, 1, styles_count) then temp = 1 end
++GUI.Target.apply_style.selected = temp
++GUI.Target.copy_tags = dt.new_widget('check_button'){
++ label = _('copy tags'),
++ value = dt.preferences.read(mod, 'active_copy_tags', 'bool'),
++ tooltip = _('copy tags from first image.'),
++ clicked_callback = function(self) dt.preferences.write(mod, 'active_copy_tags', 'bool', self.value) end,
++ reset_callback = function(self) self.value = true end
++}
++temp = dt.preferences.read(mod, 'active_add_tags', 'string')
++if temp == '' then temp = nil end
++GUI.Target.add_tags = dt.new_widget('entry'){
++ tooltip = _('additional tags to be added on import, seperate with commas, all spaces will be removed'),
++ text = temp,
++ placeholder = _('enter tags, separated by commas'),
++ editable = true
++}
++temp = dt.preferences.read(mod, 'active_current_preset_ind', 'integer')
++if not InRange(temp, 1, 6) then temp = 1 end
++GUI.Presets.current_preset = dt.new_widget('combobox'){
++ label = _('active preset'),
++ tooltip = _('preset to be loaded from or saved to'),
++ value = temp,
++ 'dri_1', 'dri_2', 'dri_3', 'dff_1', 'dff_2', 'dff_3',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_current_preset', 'string', self.value)
++ dt.preferences.write(mod, 'active_current_preset_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 1
++ dt.preferences.write(mod, 'active_current_preset', 'string', self.value)
++ dt.preferences.write(mod, 'active_current_preset_ind', 'integer', self.selected)
++ end
++}
++GUI.Presets.load = dt.new_widget('button'){
++ label = _('load fusion preset'),
++ tooltip = _('load current fusion parameters from selected preset'),
++ clicked_callback = function() LoadFromPreference(GUI.Presets.current_preset.value) end
++}
++GUI.Presets.save = dt.new_widget('button'){
++ label = _('save to fusion preset'),
++ tooltip = _('save current fusion parameters to selected preset'),
++ clicked_callback = function() SaveToPreference(GUI.Presets.current_preset.value) end
++}
++GUI.Presets.variants = dt.new_widget('check_button'){
++ label = _('create image variants from presets'),
++ value = dt.preferences.read(mod, 'active_variants', 'bool'),
++ tooltip = _('create multiple image variants based on the three different presets of the specified type'),
++ clicked_callback = function(self)
++ dt.preferences.write(mod, 'active_variants', 'bool', self.value)
++ if self.value then
++ GUI.Target.on_conflict.selected = 1
++ GUI.Target.on_conflict.sensitive = false
++ GUI.Presets.variants_type.sensitive = true
++ else
++ GUI.Target.on_conflict.sensitive = true
++ GUI.Presets.variants_type.sensitive = false
++ end
++ end,
++ reset_callback = function(self) self.value = false end
++}
++temp = dt.preferences.read(mod, 'active_variants_type_ind', 'integer')
++if not InRange(temp, 1, 2) then temp = 1 end
++GUI.Presets.variants_type = dt.new_widget('combobox'){
++ label = _('create variants type'),
++ tooltip = _('preset type to be used when creating image variants'),
++ selected = temp,
++ 'dri', 'dff',
++ changed_callback = function(self)
++ dt.preferences.write(mod, 'active_variants_type', 'string', self.value)
++ dt.preferences.write(mod, 'active_variants_type_ind', 'integer', self.selected)
++ end,
++ reset_callback = function(self)
++ self.selected = 1
++ dt.preferences.write(mod, 'active_variants_type', 'string', self.value)
++ dt.preferences.write(mod, 'active_variants_type_ind', 'integer', self.selected)
++ end
++}
++GUI.Presets.variants_type.sensitive = GUI.Presets.variants.value
++temp = df.get_executable_path_preference(AIS.name)
++GUI.exes.align_image_stack = dt.new_widget('file_chooser_button'){
++ title = 'align_image_stack ' .. _('binary path'),
++ value = temp,
++ tooltip = temp,
++ is_directory = false,
++ changed_callback = function(self) self.tooltip = self.value end
++}
++temp = df.get_executable_path_preference(ENF.name)
++GUI.exes.enfuse = dt.new_widget('file_chooser_button'){
++ title = 'enfuse ' .. _('binary path'),
++ value = temp,
++ tooltip = temp,
++ is_directory = false,
++ changed_callback = function(self) self.tooltip = self.value end
++}
++temp = df.get_executable_path_preference(EXF.name)
++GUI.exes.exiftool = dt.new_widget('file_chooser_button'){
++ title = 'exiftool ' .. _('binary path'),
++ value = temp,
++ tooltip = temp,
++ is_directory = false,
++ changed_callback = function(self) self.tooltip = self.value end
++}
++GUI.exes.update = dt.new_widget('button'){
++ label = _('update'),
++ tooltip = _('update the binary paths with current values'),
++ clicked_callback = function() ExeUpdate({AIS,ENF,EXF}) end
++}
++temp = GUI.Target.format.value
++if temp == 'tif' then temp = 1
++elseif temp == 'jpg' then temp = 2
++else temp = 3
++end
++stack_compression.active = temp
++for _,widget in pairs(GUI.AIS) do
++ widget.sensitive = GUI.align.value
++end
++GUI.Target.apply_style.sensitive = GUI.Target.auto_import.value
++GUI.Target.copy_tags.sensitive = GUI.Target.auto_import.value
++GUI.Target.add_tags.sensitive = GUI.Target.auto_import.value
++
++local box_AIS = dt.new_widget('box'){
++ orientation = 'vertical',
++ label_AIS_options,
++ GUI.align,
++ GUI.AIS.radial_distortion,
++ GUI.AIS.optimize_field,
++ GUI.AIS.optimize_image_center,
++ GUI.AIS.auto_crop,
++ GUI.AIS.distortion,
++ GUI.AIS.gpu,
++ GUI.AIS.grid_size,
++ GUI.AIS.control_points,
++ GUI.AIS.control_points_remove,
++ GUI.AIS.correlation
++}
++local box_ENF = dt.new_widget('box'){
++ orientation = 'vertical',
++ label_ENF_options,
++ GUI.ENF.exposure_weight,
++ GUI.ENF.saturation_weight,
++ GUI.ENF.contrast_weight,
++ GUI.ENF.exposure_optimum,
++ GUI.ENF.exposure_width,
++ GUI.ENF.hard_masks,
++ GUI.ENF.save_masks,
++ GUI.ENF.contrast_window_size,
++ GUI.ENF.contrast_edge_scale,
++ GUI.ENF.contrast_min_curvature,
++ GUI.Presets.current_preset,
++ GUI.Presets.load,
++ GUI.Presets.save
++}
++local box_Target = dt.new_widget('box'){
++ orientation = 'vertical',
++ label_target_options,
++ GUI.Target.format,
++ GUI.Target.depth,
++ stack_compression,
++ label_directory,
++ GUI.Target.output_directory,
++ GUI.Target.source_location,
++ GUI.Target.on_conflict,
++ GUI.Presets.variants,
++ GUI.Presets.variants_type,
++ GUI.Target.auto_import,
++ GUI.Target.apply_style,
++ GUI.Target.copy_tags,
++ GUI.Target.add_tags
++}
++local box_exes = dt.new_widget('box'){
++ orientation = 'vertical',
++ GUI.exes.align_image_stack,
++ GUI.exes.enfuse,
++ GUI.exes.exiftool,
++ GUI.exes.update
++}
++GUI.options_contain = dt.new_widget('stack'){
++ box_AIS,
++ box_ENF,
++ box_Target,
++ box_exes,
++ active = 2
++}
++GUI.show_options = dt.new_widget('combobox'){
++ label = _('show options'),
++ tooltip = _('show options for specified aspect of output'),
++ selected = 2,
++ 'align image stack', 'enfuse/enblend', 'target file',
++ changed_callback = function(self)
++ GUI.options_contain.active = self.selected
++ end,
++ reset_callback = function(self)
++ self.selected = 1
++ dt.preferences.write(mod, 'active_current_preset', 'string', self.value)
++ dt.preferences.write(mod, 'active_current_preset_ind', 'integer', self.selected)
++ end
++}
++local storage_widget = dt.new_widget('box') {
++ orientation = 'vertical',
++ GUI.show_options,
++ GUI.options_contain
++}
++
++-- Register new storage --
++dt.register_storage(
++ 'module_enfuseAdvanced', --Module name
++ _('DRI or DFF image'), --Name
++ show_status, --store: called once per exported image
++ main, --finalize: called once when all images have finished exporting
++ support_format, --supported
++ initial, --initialize
++ storage_widget
++)
++
++if dt.preferences.read(mod, 'bin_exists', 'bool') then
++ GUI.options_contain.active = 2
++ GUI.show_options.sensitive = true
++else
++ GUI.options_contain.active = 4
++ GUI.show_options.sensitive = false
++end
++
++script_data.destroy = destroy
++
++return script_data
+diff -urNr darktable-5.6.0-orig/src/external/lua-scripts/contrib/face_recognition.lua darktable-5.6.0/src/external/lua-scripts/contrib/face_recognition.lua
+--- darktable-5.6.0-orig/src/external/lua-scripts/contrib/face_recognition.lua 2026-06-12 05:06:38.000000000 +0200
++++ darktable-5.6.0/src/external/lua-scripts/contrib/face_recognition.lua 2026-09-01 09:42:04.371552681 +0200
+@@ -311,25 +311,26 @@
+ if ignoreByTag (img, ignoreTags) then
+ dt.print_log("Face recognition: Ignoring image with ID " .. img.id)
+ else
++ local tag_name = t
+ -- Check of unrecognized unknown_person
+- if t == "unknown_person" then
+- t = unknownTag
++ if tag_name == "unknown_person" then
++ tag_name = unknownTag
+ end
+ -- Check of unrecognized no_persons_found
+- if t == "no_persons_found" then
+- t = nonpersonsfoundTag
++ if tag_name == "no_persons_found" then
++ tag_name = nonpersonsfoundTag
+ end
+- if t ~= "" and t ~= nil then
+- if categoryTagString ~= "" and t ~= nonpersonsfoundTag then
+- t = categoryTagString .. "|" .. t
++ if tag_name ~= "" and tag_name ~= nil then
++ if categoryTagString ~= "" and tag_name ~= nonpersonsfoundTag then
++ tag_name = categoryTagString .. "|" .. tag_name
+ end
+- dt.print_log ("ImgId:" .. img.id .. " Tag:".. t)
++ dt.print_log ("ImgId:" .. img.id .. " Tag:".. tag_name)
+ -- Create tag if it does not exist
+- if tags_list[t] == nil then
+- tag = dt.tags.create (t)
+- tags_list[t] = tag
++ if tags_list[tag_name] == nil then
++ tag = dt.tags.create (tag_name)
++ tags_list[tag_name] = tag
+ else
+- tag = tags_list[t]
++ tag = tags_list[tag_name]
+ end
+ img:attach_tag (tag)
+ end
+diff -urNr darktable-5.6.0-orig/src/external/lua-scripts/contrib/gimp.lua darktable-5.6.0/src/external/lua-scripts/contrib/gimp.lua
+--- darktable-5.6.0-orig/src/external/lua-scripts/contrib/gimp.lua 2026-06-12 05:06:38.000000000 +0200
++++ darktable-5.6.0/src/external/lua-scripts/contrib/gimp.lua 2026-09-01 09:42:04.371924359 +0200
+@@ -142,8 +142,8 @@
+ img_list = ""
+
+ for _,exp_img in pairs(image_table) do
+- exp_img = df.sanitize_filename(exp_img)
+- img_list = img_list ..exp_img.. " "
++ local sanitized = df.sanitize_filename(exp_img)
++ img_list = img_list ..sanitized.. " "
+ end
+
+ dt.print(_("launching GIMP..."))
+diff -urNr darktable-5.6.0-orig/src/external/lua-scripts/contrib/HDRMerge.lua darktable-5.6.0/src/external/lua-scripts/contrib/HDRMerge.lua
+--- darktable-5.6.0-orig/src/external/lua-scripts/contrib/HDRMerge.lua 2026-06-12 05:06:38.000000000 +0200
++++ darktable-5.6.0/src/external/lua-scripts/contrib/HDRMerge.lua 2026-09-01 09:42:04.369289028 +0200
+@@ -277,9 +277,9 @@
+ local set_tag = GUI.Target.add_tags.text
+ if set_tag ~= nil then -- add additional user-specified tags
+ for tag in string.gmatch(set_tag, '[^,]+') do
+- tag = CleanSpaces(tag)
+- tag = dt.tags.create(tag)
+- dt.tags.attach(tag, imported)
++ local cleaned_tag = CleanSpaces(tag)
++ cleaned_tag = dt.tags.create(cleaned_tag)
++ dt.tags.attach(cleaned_tag, imported)
+ end
+ end
+ dt.print(_('HDRMerge completed successfully'))
+diff -urNr darktable-5.6.0-orig/src/external/lua-scripts/contrib/image_stack.lua darktable-5.6.0/src/external/lua-scripts/contrib/image_stack.lua
+--- darktable-5.6.0-orig/src/external/lua-scripts/contrib/image_stack.lua 2026-06-12 05:06:38.000000000 +0200
++++ darktable-5.6.0/src/external/lua-scripts/contrib/image_stack.lua 2026-09-01 09:42:04.372246585 +0200
+@@ -323,8 +323,8 @@
+ dt.print_log("image list is " .. img_list)
+ files = du.split(img_list, " ")
+ for _,f in ipairs(files) do
+- f = string.gsub(f, '[\'\"]', "")
+- os.remove(f)
++ local cleaned = string.gsub(f, '[\'\"]', "")
++ os.remove(cleaned)
+ end
+ end
+
+diff -urNr darktable-5.6.0-orig/src/external/lua-scripts/tools/get_libdoc.lua darktable-5.6.0/src/external/lua-scripts/tools/get_libdoc.lua
+--- darktable-5.6.0-orig/src/external/lua-scripts/tools/get_libdoc.lua 2026-06-12 05:06:38.000000000 +0200
++++ darktable-5.6.0/src/external/lua-scripts/tools/get_libdoc.lua 2026-09-01 09:42:04.372696230 +0200
+@@ -40,8 +40,8 @@
+ -- loop through the libraries
+
+ for line in output:lines() do
+- line = string.gsub(line, "/", ".")
+- local lib_name = line:sub(3,-5)
++ local cleaned_line = string.gsub(line, "/", ".")
++ local lib_name = cleaned_line:sub(3,-5)
+ if lib_name:len() > 2 then
+ lib_name = "lib/" .. lib_name
+ local lib = require(lib_name)
+diff -urNr darktable-5.6.0-orig/src/external/lua-scripts/tools/get_lib_manpages.lua darktable-5.6.0/src/external/lua-scripts/tools/get_lib_manpages.lua
+--- darktable-5.6.0-orig/src/external/lua-scripts/tools/get_lib_manpages.lua 2026-06-12 05:06:38.000000000 +0200
++++ darktable-5.6.0/src/external/lua-scripts/tools/get_lib_manpages.lua 2026-09-01 09:42:04.372530439 +0200
+@@ -63,8 +63,8 @@
+ -- loop through the libraries
+
+ for line in output:lines() do
+- line = string.gsub(line, "/", ".")
+- local lib_name = line:sub(3,-5)
++ local cleaned_line = string.gsub(line, "/", ".")
++ local lib_name = cleaned_line:sub(3,-5)
+ if lib_name:len() > 2 then
+ lib_name = "lib/" .. lib_name
+ local lib = require(lib_name)
+diff -urNr darktable-5.6.0-orig/src/external/lua-scripts/tools/script_manager.lua darktable-5.6.0/src/external/lua-scripts/tools/script_manager.lua
+--- darktable-5.6.0-orig/src/external/lua-scripts/tools/script_manager.lua 2026-06-12 05:06:38.000000000 +0200
++++ darktable-5.6.0/src/external/lua-scripts/tools/script_manager.lua 2026-09-01 09:42:04.372897679 +0200
+@@ -946,7 +946,9 @@
+ sm.widgets.folder_selector.selected = i
+ break
+ end
+- i = i + 1
++ -- Lua 5.5: for loop control variables are read-only. This increment
++ -- was always a no-op since numeric for loops use an internal counter.
++ -- i = i + 1
+ end
+
+ log.msg(log.debug, "clearing text fields")
+diff -urNr darktable-5.6.0-orig/src/external/lua-scripts/tools/script_manager.lua.orig darktable-5.6.0/src/external/lua-scripts/tools/script_manager.lua.orig
+--- darktable-5.6.0-orig/src/external/lua-scripts/tools/script_manager.lua.orig 1970-01-01 01:00:00.000000000 +0100
++++ darktable-5.6.0/src/external/lua-scripts/tools/script_manager.lua.orig 2026-06-12 05:06:38.000000000 +0200
+@@ -0,0 +1,1654 @@
++--[[
++ This file is part of darktable,
++ copyright (c) 2018, 2020, 2023, 2024 Bill Ferguson <wpferguson@gmail.com>
++
++ darktable 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, either version 3 of the License, or
++ (at your option) any later version.
++
++ darktable 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.
++
++ You should have received a copy of the GNU General Public License
++ along with darktable. If not, see <http://www.gnu.org/licenses/>.
++]]
++--[[
++ script_manager.lua - a tool for managing the darktable lua scripts
++
++ script_manager is designed to be called from the users luarc file and used to
++ manage the lua scripts.
++
++ On startup script_manager scans the lua scripts directory to see what scripts are present.
++ Scripts are sorted by 'folder' based on what sub-directory they are in. With no
++ additional script repositories iinstalled, the folders are contrib, examples, official
++ and tools. When a folder is selected the buttons show the script name and whether the
++ script is started or stopped. The button is a toggle, so if the script is stopped click
++ the button to start it and vice versa.
++
++ Features
++
++ * the number of script buttons shown can be changed to any number between 5 and 20. The
++ default is 10 buttons. This can be changed in the configuration action.
++
++ * additional repositories of scripts may be installed using from the install/update action.
++
++ * installed scripts can be updated from the install/update action. This includes extra
++ repositories that have been installed.
++
++ * the scripts can be disabled if desired from the install/update action. This can only
++ be reversed manually. To enable the "Disable Scripts" button, check the checkbox to
++ endable it. This is to prevent accidentally disabling the scripts. Click the
++ "Disable Scripts" button and the luarc file is renamed to luarc.disable. If at
++ a later time you want to enable the scripts again, simply rename the luarc.disabled
++ file to luarc and the scripts will run.
++
++]]
++
++
++local dt = require "darktable"
++local du = require "lib/dtutils"
++local df = require "lib/dtutils.file"
++local ds = require "lib/dtutils.string"
++local dtsys = require "lib/dtutils.system"
++local log = require "lib/dtutils.log"
++local dtdebug = require "darktable.debug"
++
++local gettext = dt.gettext.gettext
++
++local function _(msgid)
++ return gettext(msgid)
++end
++
++-- figure out where we are running from
++
++local info = debug.getinfo(1, "S")
++local scripts_path = info.source:sub(2)
++
++-- assume user based
++local system_based = false
++
++if string.match(scripts_path, ds.sanitize_lua(dt.configuration.data_dir)) then
++ log.msg(log.info, "script_manager using system based scripts")
++ system_based = true
++end
++
++-- api check
++
++-- du.check_min_api_version("9.3.0", "script_manager")
++
++-- - - - - - - - - - - - - - - - - - - - - - - -
++-- C O N S T A N T S
++-- - - - - - - - - - - - - - - - - - - - - - - -
++
++-- script_manager required API version
++local SM_API_VER_REQD <const> = "9.7.0"
++
++-- path separator
++local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
++
++-- command separator
++local CS <const> = dt.configuration.running_os == "windows" and "&" or ";"
++
++local MODULE <const> = "script_manager"
++
++local MIN_BUTTONS_PER_PAGE <const> = 5
++local MAX_BUTTONS_PER_PAGE <const> = 20
++local DEFAULT_BUTTONS_PER_PAGE <const> = 10
++
++local DEFAULT_LOG_LEVEL <const> = log.info
++
++local USER_LUA_DIR <const> = dt.configuration.config_dir .. PS .. "lua"
++local SYSTEM_LUA_DIR <const> = dt.configuration.data_dir .. PS .. "lua-scripts"
++
++local LUA_SCRIPT_REPO <const> = "https://github.com/darktable-org/lua-scripts.git"
++
++local LUA_API_VER <const> = "API-" .. dt.configuration.api_version_string
++
++-- local POWER_ICON = dt.configuration.config_dir .. "/lua/data/data/icons/power.png"
++local USER_POWER_ICON <const> = dt.configuration.config_dir .. "/lua/data/icons/path20.png"
++local USER_BLANK_ICON <const> = dt.configuration.config_dir .. "/lua/data/icons/blank20.png"
++
++local SYSTEM_POWER_ICON <const> = dt.configuration.data_dir .. "/lua-scripts/data/icons/path20.png"
++local SYSTEM_BLANK_ICON <const> = dt.configuration.data_dir .. "/lua-scripts/data/icons/blank20.png"
++
++local LUA_DIR = USER_LUA_DIR
++local POWER_ICON = USER_POWER_ICON
++local BLANK_ICON = USER_BLANK_ICON
++
++if system_based then
++ LUA_DIR = SYSTEM_LUA_DIR
++ POWER_ICON = SYSTEM_POWER_ICON
++ BLANK_ICON = SYSTEM_BLANK_ICON
++end
++-- - - - - - - - - - - - - - - - - - - - - - - -
++-- P R E F E R E N C E S
++-- - - - - - - - - - - - - - - - - - - - - - - -
++
++dt.preferences.register(MODULE, "disable_scripts", "bool",
++ _("disable Lua scripts"),
++ _("do not run the Lua scripts"),
++ false)
++
++ if dt.preferences.read(MODULE, "disable_scripts", "bool") then
++ return
++ end
++
++dt.preferences.register(MODULE, "check_update", "bool",
++ _("check for updated scripts on start up"),
++ _("automatically update scripts to correct version"),
++ true)
++
++local check_for_updates = dt.preferences.read(MODULE, "check_update", "bool")
++
++-- - - - - - - - - - - - - - - - - - - - - - - -
++-- L O G L E V E L
++-- - - - - - - - - - - - - - - - - - - - - - - -
++
++local old_log_level = log.log_level()
++
++log.log_level(DEFAULT_LOG_LEVEL)
++
++-- - - - - - - - - - - - - - - - - - - - - - - -
++-- N A M E S P A C E
++-- - - - - - - - - - - - - - - - - - - - - - - -
++
++local script_manager = {}
++local sm = script_manager
++
++sm.executables = {}
++sm.executables.git = df.check_if_bin_exists("git")
++
++sm.module_installed = false
++sm.event_registered = false
++
++-- set up tables to contain all the widgets and choices
++
++sm.widgets = {}
++sm.folders = {}
++sm.translated_folders = {}
++
++-- set log level for functions
++
++sm.log_level = DEFAULT_LOG_LEVEL
++
++--[[
++
++ sm.scripts is a table of tables for containing the scripts
++ It is organized as into folder (folder) subtables containing
++ each script definition, which is a table
++
++ sm.scripts-
++ |
++ - folder------------|
++ | - script
++ - folder----| |
++ - script|
++ | - script
++ - script|
++
++ and a script table looks like
++
++ name the name of the script file without the lua extension
++ path folder (folder), path separator, path, name without the lua extension
++ doc the header comments from the script to be used as a tooltip
++ script_name the folder, path separator, and name without the lua extension
++ running true if running, false if not, hidden if running but the
++ lib/storage/action for the script is hidden
++ has_lib true if it creates a module
++ lib_name name of the created lib
++ has_storage true if it creates a storage (exporter)
++ storage_name name of the exporter (in the exporter storage menu)
++ has_action true if it creates an action
++ action_name name on the button
++ has_select true if it creates a select
++ select_name name on the button
++ has_event true if it creates an event handler
++ event_type type of event, shortcut, post-xxx, pre-xxx
++ callback name of the callback routine
++ initialized all of the above data has been retreived and set. If the
++ script is unloaded and reloaded we don't have to reparse the file
++
++]]
++
++sm.scripts = {}
++sm.start_queue = {}
++sm.page_status = {}
++sm.page_status.num_buttons = DEFAULT_BUTTONS_PER_PAGE
++sm.page_status.buttons_created = 0
++sm.page_status.current_page = 0
++sm.page_status.folder = ""
++
++-- installed script repositories
++sm.installed_repositories = {
++ {name = "lua-scripts", directory = LUA_DIR},
++}
++
++
++-- don't let it run until everything is in place
++sm.run = false
++
++-- - - - - - - - - - - - - - - - - - - - - - - -
++-- F U N C T I O N S
++-- - - - - - - - - - - - - - - - - - - - - - - -
++
++-------------------
++-- helper functions
++-------------------
++
++local function set_log_level(level)
++ local old_log_level = log.log_level()
++ log.log_level(level)
++ return old_log_level
++end
++
++local function restore_log_level(level)
++ log.log_level(level)
++end
++
++local function pref_read(name, pref_type)
++ local old_log_level = set_log_level(sm.log_level)
++
++ log.msg(log.debug, "name is " .. name .. " and type is " .. pref_type)
++
++ local val = dt.preferences.read(MODULE, name, pref_type)
++
++ log.msg(log.debug, "read value " .. tostring(val))
++
++ restore_log_level(old_log_level)
++ return val
++end
++
++local function pref_write(name, pref_type, value)
++ local old_log_level = set_log_level(sm.log_level)
++
++ log.msg(log.debug, "writing value " .. tostring(value) .. " for name " .. name)
++
++ dt.preferences.write(MODULE, name, pref_type, value)
++
++ restore_log_level(old_log_level)
++end
++
++----------------
++-- git interface
++----------------
++
++local function get_repo_status(repo)
++ local old_log_level = set_log_level(sm.log_level)
++
++ local p = io.popen("cd " .. repo .. CS .. "git status")
++
++ if p then
++ local data = p:read("*a")
++ p:close()
++ return data
++ end
++
++ log.msg(log.error, "unable to get status of " .. repo)
++ restore_log_level(old_log_level)
++ return nil
++end
++
++local function get_current_repo_branch(repo)
++ local old_log_level = set_log_level(sm.log_level)
++
++ local branch = nil
++
++ local p = io.popen("cd " .. repo .. CS .. "git branch --all")
++
++ if p then
++ local data = p:read("*a")
++ p:close()
++ local branches = du.split(data, "\n")
++ for _, b in ipairs(branches) do
++ log.msg(log.debug, "branch for testing is " .. b)
++ branch = string.match(b, "^%* (.-)$")
++
++ if branch then
++ log.msg(log.info, "current repo branch is " .. branch)
++ return branch
++ end
++
++ end
++ end
++
++ if not branch then
++ log.msg(log.error, "no current branch detected in repo_data")
++ end
++
++ restore_log_level(old_log_level)
++ return nil
++end
++
++local function get_repo_branches(repo)
++ local old_log_level = set_log_level(sm.log_level)
++
++ local branches = {}
++ local p = io.popen("cd " .. repo .. CS .. "git pull --all" .. CS .. "git branch --all")
++
++ if p then
++ local data = p:read("*a")
++ p:close()
++ log.msg(log.debug, "data is \n" .. data)
++ local branch_data = du.split(data, "\n")
++ for _, line in ipairs(branch_data) do
++ log.msg(log.debug, "line is " .. line)
++ local branch = string.gsub(line, "%s+remotes/%a+/", "")
++ if string.match(branch, "API") then
++ log.msg(log.info, "found branch - " .. branch)
++ table.insert(branches, branch)
++ end
++ end
++ end
++
++ restore_log_level(old_log_level)
++ return branches
++end
++
++local function is_repo_clean(repo_data)
++ local old_log_level = set_log_level(sm.log_level)
++
++ if string.match(repo_data, "\n%s-%a.-%a:%s-%a%g-\n") then
++ log.msg(log.info, "repo is dirty")
++ return false
++ else
++ log.msg(log.info, "repo is clean")
++ return true
++ end
++
++ restore_log_level(old_log_level)
++end
++
++local function checkout_repo_branch(repo, branch)
++ local old_log_level = set_log_level(sm.log_level)
++
++ log.msg(log.info, "checkout out branch " .. branch .. " from repository " .. repo)
++
++ os.execute("cd " .. repo .. CS .. "git checkout " .. branch)
++
++ restore_log_level(old_log_level)
++end
++
++--------------------
++-- utility functions
++--------------------
++
++local function update_combobox_choices(combobox, choice_table, selected)
++ local old_log_level = set_log_level(sm.log_level)
++
++ local items = #combobox
++ local choices = #choice_table
++
++ for i, name in ipairs(choice_table) do
++ combobox[i] = name
++ end
++
++ if choices < items then
++ for j = items, choices + 1, -1 do
++ combobox[j] = nil
++ end
++ end
++
++ if not selected then
++ selected = 1
++ end
++
++ combobox.value = selected
++ restore_log_level(old_log_level)
++end
++
++local function string_trim(str)
++ local old_log_level = set_log_level(sm.log_level)
++
++ local result = string.gsub(str, "^%s+", "") -- trim leading spaces
++ result = string.gsub(result, "%s+$", "") -- trim trailing spaces
++ result = string.gsub(result, ",?%s+%-%-.+$", "") -- trim trailing comma and comments
++
++ restore_log_level(old_log_level)
++ return result
++end
++
++local function string_dequote(str)
++ return string.gsub(str, "['\"]", "")
++end
++
++local function string_dei18n(str)
++ return string.match(str, "%_%((.+)%)")
++end
++
++local function string_chop(str)
++ return str:sub(1, -2)
++end
++
++------------------
++-- script handling
++------------------
++
++local function is_folder_known(folder_table, name)
++ local match = false
++
++ for _, folder_name in ipairs(folder_table) do
++ if name == folder_name then
++ match = true
++ end
++ end
++
++ return match
++end
++
++local function find_translated_name(folder)
++ local translated_name = nil
++
++ if folder == "contrib" then
++ translated_name = _("contributed")
++ elseif folder == "examples" then
++ translated_name = _("examples")
++ elseif folder == "official" then
++ translated_name = _("official")
++ elseif folder == "tools" then
++ translated_name = _("tools")
++ else
++ translated_name = _(folder) -- in case we get lucky and the string got translated elsewhere
++ end
++
++ return translated_name
++end
++
++local function add_script_folder(folder)
++ local old_log_level = set_log_level(sm.log_level)
++
++ if #sm.folders == 0 or not is_folder_known(sm.folders, folder) then
++ table.insert(sm.folders, folder)
++ table.insert(sm.translated_folders, find_translated_name(folder))
++ sm.scripts[folder] = {}
++ log.msg(log.debug, "created folder " .. folder)
++ end
++
++ restore_log_level(old_log_level)
++end
++
++local function get_script_metadata(script)
++ local old_log_level = set_log_level(sm.log_level)
++ -- set_log_level(log.debug)
++
++ log.msg(log.debug, "processing metatdata for " .. script)
++
++ local metadata_block = nil
++ local metadata = {}
++
++ f = io.open(LUA_DIR .. PS .. script .. ".lua")
++ if f then
++ -- slurp the file
++ local content = f:read("*all")
++ f:close()
++ -- grab the script_data.metadata table
++ metadata_block = string.match(content, "script_data%.metadata = %{\r?\n(.-)\r?\n%}")
++ else
++ log.msg(log.error, "cant read from " .. script)
++ end
++
++ if metadata_block then
++ -- break up the lines into key value pairs
++ local lines = du.split(metadata_block, "\n")
++ log.msg(log.debug, "got " .. #lines .. " lines")
++ for i = 1, #lines do
++ log.msg(log.debug, "splitting line " .. lines[i])
++ local parts = du.split(lines[i], " = ")
++ parts[1] = string_trim(parts[1])
++ parts[2] = string_trim(parts[2])
++ log.msg(log.debug, "got value " .. parts[1] .. " and data " .. parts[2])
++ if string.match(parts[2], "%_%(") then
++ parts[2] = _(string_dequote(string_dei18n(parts[2])))
++ else
++ parts[2] = string_dequote(parts[2])
++ end
++ if string.match(parts[2], ",$") then
++ parts[2] = string_chop(parts[2])
++ end
++ log.msg(log.debug, "parts 1 is " .. parts[1] .. " and parts 2 is " .. parts[2])
++ metadata[parts[1]] = parts[2]
++ log.msg(log.debug, "metadata " .. parts[1] .. " is " .. metadata[parts[1]])
++ end
++ log.msg(log.debug, "script data found for " .. metadata["name"])
++ end
++
++ restore_log_level(old_log_level)
++ return metadata_block and metadata or nil
++end
++
++local function get_script_doc(script)
++ local old_log_level = set_log_level(sm.log_level)
++ local description = nil
++ f = io.open(LUA_DIR .. PS .. script .. ".lua")
++ if f then
++ -- slurp the file
++ local content = f:read("*all")
++ f:close()
++ -- assume that the second block comment is the documentation
++ description = string.match(content, "%-%-%[%[.-%]%].-%-%-%[%[(.-)%]%]")
++ else
++ log.msg(log.error, "can't read from " .. script)
++ end
++ if description then
++ restore_log_level(old_log_level)
++ return description
++ else
++ restore_log_level(old_log_level)
++ return _("no documentation available")
++ end
++end
++
++local function activate(script)
++ local old_log_level = set_log_level(sm.log_level)
++
++ local status = nil -- status of start function
++ local err = nil -- error message returned if module doesn't start
++
++ log.msg(log.info, "activating " .. script.name)
++
++ if script.running == false then
++
++ script_manager_running_script = script.name
++
++ status, err = du.prequire(script.path)
++ log.msg(log.debug, "prequire returned " .. tostring(status) .. " and for err " .. tostring(err))
++
++ script_manager_running_script = nil
++
++ if status then
++ pref_write(script.script_name, "bool", true)
++ log.msg(log.screen, _(string.format("loaded %s", script.script_name)))
++ script.running = true
++
++ if err ~= true then
++ log.msg(log.debug, "got lib data")
++ script.data = err
++ if script.data.destroy_method and script.data.destroy_method == "hide" and script.data.show and dt.gui.current_view().id == "lighttable" then
++ script.data.show()
++ end
++ else
++ script.data = nil
++ end
++
++ else
++ log.msg(log.screen, _(string.format("%s failed to load", script.script_name)))
++ log.msg(log.error, "error loading " .. script.script_name)
++ log.msg(log.error, "error message: " .. err)
++ end
++
++ else -- script is a lib and loaded but hidden and the user wants to reload
++ script.data.restart()
++ script.running = true
++ status = true
++ pref_write(script.script_name, "bool", true)
++ end
++ script_manager_running_script = "script_manager"
++
++ restore_log_level(old_log_level)
++ return status
++end
++
++local function deactivate(script)
++ -- presently the lua api doesn't support unloading lib elements however, we
++ -- can hide libs, so we just mark those as hidden and hide the gui
++ -- can delete storages
++ -- can delete actions
++ -- can delete selects
++ -- and mark them inactive for the next time darktable starts
++
++ -- deactivate it....
++ local old_log_level = set_log_level(sm.log_level)
++
++ pref_write(script.script_name, "bool", false)
++
++ if script.data then
++
++ script.data.destroy()
++
++ if script.data.destroy_method then
++ if string.match(script.data.destroy_method, "hide") then
++ script.running = "hidden"
++ else
++ package.loaded[script.script_name] = nil
++ script.running = false
++ end
++ else
++ package.loaded[script.script_name] = nil
++ script.running = false
++ end
++
++ log.msg(log.info, "turned off " .. script.script_name)
++ log.msg(log.screen, _(string.format("%s stopped", script.name)))
++
++ else
++ script.running = false
++
++ log.msg(log.info, "setting " .. script.script_name .. " to not start")
++ log.msg(log.screen, _(string.format("%s will not start when darktable is restarted", script.name)))
++ end
++
++ restore_log_level(old_log_level)
++end
++
++local function start_scripts()
++ for _, script in ipairs(sm.start_queue) do
++ activate(script)
++ for i = 1, sm.page_status.num_buttons do
++ local name = script.metadata and script.metadata.name or script.name
++ if sm.widgets.labels[i].label == name then
++ sm.widgets.buttons[i].name = "pb_on"
++ break
++ end
++ end
++ end
++ sm.start_queue = {}
++end
++
++local function queue_script_to_start(script)
++ table.insert(sm.start_queue, script)
++end
++
++local function add_script_name(name, path, folder)
++ local old_log_level = set_log_level(sm.log_level)
++
++ log.msg(log.debug, "folder is " .. folder)
++ log.msg(log.debug, "name is " .. name)
++
++ local script = {
++ name = name,
++ path = folder .. "/" .. path .. name,
++ running = false,
++ doc = get_script_doc(folder .. "/" .. path .. name),
++ metadata = get_script_metadata(folder .. "/" .. path .. name),
++ script_name = folder .. "/" .. name,
++ data = nil
++ }
++
++ table.insert(sm.scripts[folder], script)
++
++ if pref_read(script.script_name, "bool") then
++ queue_script_to_start(script)
++ else
++ pref_write(script.script_name, "bool", false)
++ end
++
++ restore_log_level(old_log_level)
++end
++
++local function process_script_data(script_file)
++ local old_log_level = set_log_level(sm.log_level)
++
++ -- the script file supplied is folder/filename.filetype
++ -- the following pattern splits the string into folder, path, name, fileename, and filetype
++ -- for example contrib/gimp.lua becomes
++ -- folder - contrib
++ -- path -
++ -- name - gimp.lua
++ -- filename - gimp
++ -- filetype - lua
++
++ -- Thanks Tobias Jakobs for the awesome regulary expression
++
++ local pattern = "(.-)/(.-)(([^\\/]-)%.?([^%.\\/]*))$"
++ if dt.configuration.running_os == "windows" then
++ -- change the path separator from / to \ for windows
++ pattern = "(.-)\\(.-)(([^\\]-)%.?([^%.\\]*))$"
++ end
++
++ log.msg(log.info, "processing " .. script_file)
++
++ -- add the script data
++ local folder,path,name,filename,filetype = string.match(script_file, pattern)
++
++ if folder and name and path then
++ log.msg(log.debug, "folder is " .. folder)
++ log.msg(log.debug, "name is " .. name)
++
++ add_script_folder(folder)
++ add_script_name(name, path, folder)
++ end
++
++ restore_log_level(old_log_level)
++end
++
++local function ensure_lib_in_search_path(line)
++ local old_log_level = set_log_level(sm.log_level)
++
++ log.msg(log.debug, "line is " .. line)
++
++ if string.match(line, ds.sanitize_lua(dt.configuration.config_dir .. PS .. "lua/lib")) then
++ log.msg(log.debug, line .. " is already in search path, returning...")
++ return
++ end
++
++ local pattern = dt.configuration.running_os == "windows" and "(.+)\\lib\\.+lua" or "(.+)/lib/.+lua"
++ local path = string.match(line, pattern)
++
++ log.msg(log.debug, "extracted path is " .. path)
++ log.msg(log.debug, "package.path is " .. package.path)
++
++ if not string.match(package.path, ds.sanitize_lua(path)) then
++
++ log.msg(log.debug, "path isn't in package.path, adding...")
++
++ package.path = package.path .. ";" .. path .. "/?.lua"
++
++ log.msg(log.debug, "new package.path is " .. package.path)
++ end
++
++ restore_log_level(old_log_level)
++end
++
++local function scan_scripts(script_dir)
++ local old_log_level = set_log_level(sm.log_level)
++
++ local script_count = 0
++ local find_cmd = "find -L " .. script_dir .. " -name \\*.lua -print | sort"
++
++ if dt.configuration.running_os == "windows" then
++ find_cmd = "dir /b/s \"" .. script_dir .. "\\*.lua\" 2>nul | sort"
++ end
++
++ log.msg(log.debug, "find command is " .. find_cmd)
++
++ -- scan the scripts
++ local output = io.popen(find_cmd)
++ for line in output:lines() do
++ log.msg(log.debug, "line is " .. line)
++ local script_dir = SYSTEM_LUA_DIR
++ if string.match(line, ds.sanitize_lua(USER_LUA_DIR)) then
++ script_dir = USER_LUA_DIR
++ end
++ -- add a string.match to figure out which lua dir to use
++ local l = string.gsub(line, ds.sanitize_lua(script_dir) .. PS, "") -- strip the lua dir off
++ local script_file = l:sub(1,-5) -- strip off .lua\n
++ if not string.match(script_file, "script_manager") then -- let's not include ourself
++ if not string.match(script_file, "plugins") then -- skip plugins
++ if not string.match(script_file, "lib" .. PS) then -- let's not try and run libraries
++ if not string.match(script_file, "%.git") then -- don't match files in the .git directory
++ process_script_data(script_file)
++ script_count = script_count + 1
++ end
++ else
++ ensure_lib_in_search_path(line) -- but let's make sure libraries can be found
++ end
++ end
++ end
++ end
++
++ restore_log_level(old_log_level)
++ return script_count
++end
++
++local function update_scripts()
++ local old_log_level = set_log_level(sm.log_level)
++ local result = false
++
++ local git = sm.executables.git
++
++ if not git then
++ log.msg(log.screen, _("ERROR: git not found. Install or specify the location of the git executable."))
++ return
++ end
++
++ local git_command = "cd " .. LUA_DIR .. " " .. CS .. " " .. git .. " pull"
++ log.msg(log.debug, "update git command is " .. git_command)
++
++ if dt.configuration.running_os == "windows" then
++ result = dtsys.windows_command(git_command)
++ else
++ result = os.execute(git_command)
++ end
++
++ if result == 0 then
++ log.msg(log.screen, _("lua scripts successfully updated"))
++ end
++
++ restore_log_level(old_log_level)
++ return result
++end
++
++--------------
++-- UI handling
++--------------
++
++local function update_script_update_choices()
++ local old_log_level = set_log_level(sm.log_level)
++
++ local installs = {}
++ local pref_string = ""
++
++ for i, repo in ipairs(sm.installed_repositories) do
++ table.insert(installs, repo.name)
++ if not string.match(pref_string, ds.sanitize_lua(repo.name)) then
++ pref_string = pref_string .. i .. "," .. repo.name .. "," .. repo.directory .. ","
++ end
++ end
++
++ update_combobox_choices(sm.widgets.update_script_choices, installs, 1)
++
++ log.msg(log.debug, "repo pref string is " .. pref_string)
++ pref_write("installed_repos", "string", pref_string)
++
++ restore_log_level(old_log_level)
++end
++
++local function scan_repositories()
++ local old_log_level = set_log_level(sm.log_level)
++
++ local script_count = 0
++ local find_cmd = "find -L " .. LUA_DIR .. " -name \\*.git -print | sort"
++
++ if dt.configuration.running_os == "windows" then
++ find_cmd = "dir /b/s /a:d " .. LUA_DIR .. PS .. "*.git 2>nul | sort"
++ end
++
++ log.msg(log.debug, "find command is " .. find_cmd)
++
++ local output = io.popen(find_cmd)
++
++ for line in output:lines() do
++ local l = string.gsub(line, ds.sanitize_lua(LUA_DIR) .. PS, "") -- strip the lua dir off
++ local folder = string.match(l, "(.-)" .. PS) -- get everything to the first /
++
++ if folder then -- if we have a folder (.git doesn't)
++
++ log.msg(log.debug, "found folder " .. folder)
++
++ if not string.match(folder, "plugins") and not string.match(folder, "%.git") then -- skip plugins
++
++ if #sm.installed_repositories == 1 then
++ log.msg(log.debug, "only 1 repo, adding " .. folder)
++ table.insert(sm.installed_repositories, {name = folder, directory = LUA_DIR .. PS .. folder})
++ else
++ log.msg(log.debug, "more than 1 repo, we have to search the repos to make sure it's not there")
++ local found = nil
++
++ for _, repo in ipairs(sm.installed_repositories) do
++ if string.match(repo.name, ds.sanitize_lua(folder)) then
++ log.msg(log.debug, "matched " .. repo.name)
++ found = true
++ break
++ end
++ end
++
++ if not found then
++ table.insert(sm.installed_repositories, {name = folder, directory = LUA_DIR .. PS .. folder})
++ end
++
++ end
++ end
++ end
++ end
++
++ update_script_update_choices()
++
++ restore_log_level(old_log_level)
++end
++
++local function install_scripts()
++ local old_log_level = set_log_level(sm.log_level)
++
++ local url = sm.widgets.script_url.text
++ local folder = sm.widgets.new_folder.text
++
++ if not df.test_file(USER_LUA_DIR, "d") then
++ df.mkdir(USER_LUA_DIR)
++ if not df.test_file(USER_LUA_DIR, "d") then
++ log.msg(log.error, "unable to create user lua directory")
++ log.msg(log.screen, _("unable to create user lua directory, installing additional scripts failed"))
++ return
++ end
++ end
++
++ if string.match(du.join(sm.folders, " "), ds.sanitize_lua(folder)) then
++ log.msg(log.screen, string.format(_("folder %s is already in use. Please specify a different folder name."), folder))
++ log.msg(log.error, "folder " .. folder .. " already exists, returning...")
++ restore_log_level(old_log_level)
++ return
++ end
++
++ local result = false
++
++ local git = sm.executables.git
++
++ if not git then
++ log.msg(log.screen, _("ERROR: git not found. Install or specify the location of the git executable."))
++ restore_log_level(old_log_level)
++ return
++ end
++
++ local git_command = "cd " .. USER_LUA_DIR .. " " .. CS .. " " .. git .. " clone " .. url .. " " .. folder
++ log.msg(log.debug, "update git command is " .. git_command)
++
++ if dt.configuration.running_os == "windows" then
++ result = dtsys.windows_command(git_command)
++ else
++ result = dtsys.external_command(git_command)
++ end
++
++ log.msg(log.info, "result from import is " .. result)
++
++ if result == 0 then
++ local count = scan_scripts(USER_LUA_DIR)
++
++ if count > 0 then
++ update_combobox_choices(sm.widgets.folder_selector, sm.folders, sm.widgets.folder_selector.selected)
++ dt.print(string.format(_("scripts successfully installed into folder %s"), folder))
++ table.insert(sm.installed_repositories, {name = folder, directory = USER_LUA_DIR .. PS .. folder})
++ update_script_update_choices()
++
++ for i = 1, #sm.widgets.folder_selector do
++ if string.match(sm.widgets.folder_selector[i], ds.sanitize_lua(folder)) then
++ log.msg(log.debug, "setting folder selector to " .. i)
++ sm.widgets.folder_selector.selected = i
++ break
++ end
++ i = i + 1
++ end
++
++ log.msg(log.debug, "clearing text fields")
++ sm.widgets.script_url.text = ""
++ sm.widgets.new_folder.text = ""
++ sm.widgets.main_menu.selected = 3
++ else
++ log.msg(log.screen, _("no scripts found to install"))
++ log.msg(log.error, "scan_scripts returned " .. count .. " scripts found. Not adding to folder_selector")
++ end
++
++ else
++ log.msg(log.screen, _("failed to download scripts"))
++ end
++
++ restore_log_level(old_log_level)
++ return result
++end
++
++local function clear_button(number)
++ local old_log_level = set_log_level(sm.log_level)
++
++ local button = sm.widgets.buttons[number]
++ local label = sm.widgets.labels[number]
++
++ button.image = BLANK_ICON
++ button.tooltip = ""
++ button.sensitive = false
++ label.label = ""
++ button.name = ""
++
++ restore_log_level(old_log_level)
++end
++
++local function find_script(folder, name)
++ local old_log_level = set_log_level(sm.log_level)
++
++ log.msg(log.debug, "looking for script " .. name .. " in folder " .. folder)
++
++ for _, script in ipairs(sm.scripts[folder]) do
++ if string.match(script.name, "^" .. ds.sanitize_lua(name) .. "$") then
++ return script
++ end
++ end
++
++ restore_log_level(old_log_level)
++ return nil
++end
++
++local function populate_buttons(folder, first, last)
++ local old_log_level = set_log_level(sm.log_level)
++
++ log.msg(log.debug, "folder is " .. folder .. " and first is " .. first .. " and last is " .. last)
++
++ local button_num = 1
++
++ for i = first, last do
++ local script = sm.scripts[folder][i]
++ local button = sm.widgets.buttons[button_num]
++ local label = sm.widgets.labels[button_num]
++
++ if script.running == true then
++ button.name = "pb_on"
++ else
++ button.name = "pb_off"
++ end
++
++ button.image = POWER_ICON
++ label.label = script.metadata and script.metadata.name or script.name
++ label.name = "pb_label"
++ button.ellipsize = "end"
++ button.sensitive = true
++ label.tooltip = script.metadata and script.metadata.purpose or script.doc
++
++ button.clicked_callback = function (this)
++ local cb_script = script
++ local state = nil
++ if cb_script then
++ log.msg(log.debug, "found script " .. cb_script.name .. " with path " .. cb_script.path)
++ if cb_script.running == true then
++ log.msg(log.debug, "deactivating " .. cb_script.name .. " on " .. cb_script.path)
++ deactivate(cb_script)
++ this.name = "pb_off"
++ else
++ log.msg(log.debug, "activating " .. cb_script.name .. " on " .. script.path)
++ local result = activate(cb_script)
++ if result then
++ this.name = "pb_on"
++ end
++ end
++ end
++ end
++
++ button_num = button_num + 1
++ end
++
++ log.msg(log.debug, "button num is " .. button_num .. " and num_buttons is " .. sm.page_status.num_buttons)
++ if button_num <= sm.page_status.num_buttons then
++ for i = button_num, sm.page_status.num_buttons do
++ clear_button(i)
++ end
++ end
++
++ restore_log_level(old_log_level)
++end
++
++local function paginate(direction)
++ local old_log_level = set_log_level(sm.log_level)
++
++ local folder = sm.page_status.folder
++ log.msg(log.debug, "folder is " .. folder)
++
++ local num_scripts = #sm.scripts[folder]
++ log.msg(log.debug, "num_scripts is " .. num_scripts)
++
++ local max_pages = math.ceil(num_scripts / sm.page_status.num_buttons)
++
++ local cur_page = sm.page_status.current_page
++ log.msg(log.debug, "max pages is " .. max_pages)
++
++ local buttons_needed = nil
++ local first = nil
++ local last = nil
++
++ if direction == 0 then
++ cur_page = cur_page - 1
++ if cur_page < 1 then
++ cur_page = 1
++ end
++ elseif direction == 1 then
++ cur_page = cur_page + 1
++ if cur_page > max_pages then
++ cur_page = max_pages
++ end
++ else
++ log.msg(log.debug, "took path 2")
++ cur_page = 1
++ end
++
++ log.msg(log.debug, "cur_page is " .. cur_page .. " and max_pages is " .. max_pages)
++
++ if cur_page == max_pages and cur_page == 1 then
++ sm.widgets.page_forward.sensitive = false
++ sm.widgets.page_back.sensitive = false
++ elseif cur_page == max_pages then
++ sm.widgets.page_forward.sensitive = false
++ sm.widgets.page_back.sensitive = true
++ elseif cur_page == 1 then
++ sm.widgets.page_forward.sensitive = true
++ sm.widgets.page_back.sensitive = false
++ else
++ sm.widgets.page_forward.sensitive = true
++ sm.widgets.page_back.sensitive = true
++ end
++
++ sm.page_status.current_page = cur_page
++
++ first = (cur_page * sm.page_status.num_buttons) - (sm.page_status.num_buttons - 1)
++
++ if first + sm.page_status.num_buttons > num_scripts then
++ last = num_scripts
++ else
++ last = first + sm.page_status.num_buttons - 1
++ end
++
++ sm.widgets.page_status.label = string.format(_("page %d of %d"), cur_page, max_pages)
++
++ populate_buttons(folder, first, last)
++
++ restore_log_level(old_log_level)
++end
++
++local function change_folder(folder)
++ local old_log_level = set_log_level(sm.log_level)
++
++ if not folder then
++ log.msg(log.debug "setting folder to selector value " .. sm.widgets.folder_selector.value)
++ sm.page_status.folder = sm.widgets.folder_selector.value
++ else
++ log.msg(log.debug, "setting catgory to argument " .. folder)
++ sm.page_status.folder = folder
++ end
++
++ paginate(2)
++
++ restore_log_level(old_log_level)
++end
++
++local function change_num_buttons()
++ local old_log_level = set_log_level(sm.log_level)
++
++ cur_buttons = sm.page_status.num_buttons
++ new_buttons = sm.widgets.num_buttons.value
++
++ pref_write("num_buttons", "integer", new_buttons)
++
++ if new_buttons < cur_buttons then
++ log.msg(log.debug, "took new is less than current branch")
++
++ for i = 1, cur_buttons - new_buttons do
++ table.remove(sm.widgets.scripts)
++ end
++
++ log.msg(log.debug, "finished removing widgets, now there are " .. #sm.widgets.buttons)
++ elseif new_buttons > cur_buttons then
++ log.msg(log.debug, "took new is greater than current branch")
++ log.msg(log.debug, "number of scripts is " .. #sm.widgets.scripts)
++ log.msg(log.debug, "number of buttons is " .. #sm.widgets.buttons)
++ log.msg(log.debug, "number of labels is " .. #sm.widgets.labels)
++ log.msg(log.debug, "number of boxes is " .. #sm.widgets.boxes)
++
++ if new_buttons > sm.page_status.buttons_created then
++
++ for i = sm.page_status.buttons_created + 1, new_buttons do
++ log.msg(log.debug, "i is " .. i)
++ table.insert(sm.widgets.buttons, dt.new_widget("button"){})
++ log.msg(log.debug, "inserted new button")
++ log.msg(log.debug, "number of buttons is " .. #sm.widgets.buttons)
++ table.insert(sm.widgets.labels, dt.new_widget("label"){})
++ log.msg(log.debug, "inserted new label")
++ log.msg(log.debug, "number of labels is " .. #sm.widgets.labels)
++ table.insert(sm.widgets.boxes, dt.new_widget("box"){ orientation = "horizontal", expand = false, fill = false,
++ sm.widgets.buttons[i], sm.widgets.labels[i]})
++ log.msg(log.debug, "inserted new box")
++ sm.page_status.buttons_created = sm.page_status.buttons_created + 1
++ end
++
++ end
++
++ log.msg(log.debug, "cur_buttons is " .. cur_buttons .. " and new_buttons is " .. new_buttons)
++ log.msg(log.debug, #sm.widgets.buttons .. " buttons are available")
++
++ for i = cur_buttons + 1, new_buttons do
++ log.msg(log.debug, "inserting button " .. i .. " into scripts widget")
++ table.insert(sm.widgets.scripts, sm.widgets.boxes[i])
++ end
++
++ log.msg(log.debug, "finished adding widgets, now there are " .. #sm.widgets.buttons)
++ else -- no change
++ log.msg(log.debug, "no change, just returning")
++ return
++ end
++
++ sm.page_status.num_buttons = new_buttons
++ log.msg(log.debug, "num_buttons set to " .. sm.page_status.num_buttons)
++ paginate(2) -- force the buttons to repopulate
++ sm.widgets.main_menu.selected = 3 -- jump back to start/stop scripts
++
++ restore_log_level(old_log_level)
++end
++
++local function load_preferences()
++ local old_log_level = set_log_level(sm.log_level)
++
++ -- load the prefs and update settings
++ -- update_script_choices
++
++ local pref_string = pref_read("installed_repos", "string")
++ local entries = du.split(pref_string, ",")
++
++ while #entries > 2 do
++ local num = table.remove(entries, 1)
++ local name = table.remove(entries, 1)
++ local directory = table.remove(entries, 1)
++
++ if not string.match(sm.installed_repositories[1].name, "^" .. ds.sanitize_lua(name) .. "$") then
++ table.insert(sm.installed_repositories, {name = name, directory = directory})
++ end
++
++ end
++
++ update_script_update_choices()
++ log.msg(log.debug, "updated installed scripts")
++
++ -- folder selector
++ local val = pref_read("folder_selector", "integer")
++
++ if val == 0 then
++ val = 1
++ end
++
++ sm.widgets.folder_selector.selected = val
++ sm.page_status.folder = sm.widgets.folder_selector.value
++ log.msg(log.debug, "updated folder selector and set it to " .. sm.widgets.folder_selector.value)
++
++ -- num_buttons
++ local val = pref_read("num_buttons", "integer")
++
++ if val == 0 then
++ val = DEFAULT_BUTTONS_PER_PAGE
++ end
++
++ sm.widgets.num_buttons.value = val
++ log.msg(log.debug, "set page buttons to " .. val)
++
++ change_num_buttons()
++ log.msg(log.debug, "paginated")
++
++ -- main menu
++ local val = pref_read("main_menu_action", "integer")
++ log.msg(log.debug, "read " .. val .. " for main menu")
++
++ if val == 0 then
++ val = 3
++ end
++
++ sm.widgets.main_menu.selected = val
++ log.msg(log.debug, "set main menu to val " .. val .. " which is " .. sm.widgets.main_menu.value)
++
++ log.msg(log.debug, "set main menu to " .. sm.widgets.main_menu.value)
++
++ restore_log_level(old_log_level)
++end
++
++local function install_module()
++ local old_log_level = set_log_level(sm.log_level)
++
++ if not sm.module_installed then
++ dt.register_lib(
++ "script_manager", -- Module name
++ _("scripts"), -- Visible name
++ true, -- expandable
++ false, -- resetable
++ {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_BOTTOM", 100}}, -- containers
++ sm.widgets.main_box,
++ nil,-- view_enter
++ nil -- view_leave
++ )
++ sm.module_installed = true
++ end
++
++ sm.run = true
++ sm.use_color = pref_read("use_color", "bool")
++ log.msg(log.debug, "set run to true, loading preferences")
++ load_preferences()
++ scan_repositories()
++ start_scripts()
++
++ restore_log_level(old_log_level)
++end
++
++-- - - - - - - - - - - - - - - - - - - - - - - -
++-- M A I N P R O G R A M
++-- - - - - - - - - - - - - - - - - - - - - - - -
++
++-- ensure shortcuts module knows widgets belong to script_manager
++
++script_manager_running_script = "script_manager"
++
++if (check_for_updates or SM_API_VER_REQD > dt.configuration.api_version_string) and not system_based then
++ local repo_data = get_repo_status(LUA_DIR)
++ local current_branch = get_current_repo_branch(LUA_DIR)
++ local clean = is_repo_clean(repo_data)
++ local repo = LUA_DIR
++
++ if current_branch then
++
++ if sm.executables.git and clean and
++
++ (current_branch == "master" or string.match(current_branch, "^API%-")) then -- only make changes to clean branches
++ local branches = get_repo_branches(LUA_DIR)
++
++ if current_branch ~= LUA_API_VER and current_branch ~= "master" then
++ -- probably upgraded from an earlier api version so get back to master
++ -- to use the latest version of script_manager to get the proper API
++ checkout_repo_branch(repo, "master")
++ log.msg(log.screen, _("lua API version reset, please restart darktable"))
++
++ elseif LUA_API_VER == current_branch then
++ -- do nothing, we are fine
++ log.msg(log.debug, "took equal branch, doing nothing")
++
++ elseif string.match(LUA_API_VER, "dev") then
++ -- we are on a dev API version, so checkout the dev
++ -- api version or checkout/stay on master
++ log.msg(log.debug, "took the dev branch")
++ local match = false
++
++ for _, branch in ipairs(branches) do
++ log.msg(log.debug, "checking branch " .. branch .. " against API " .. LUA_API_VER)
++ if LUA_API_VER == branch then
++ match = true
++ log.msg(log.info, "checking out repo development branch " .. branch)
++ checkout_repo_branch(repo, branch)
++ end
++ end
++
++ if not match then
++ if current_branch == "master" then
++ log.msg(log.info, "staying on master, no dev branch yet")
++ else
++ log.msg(log.info, "no dev branch available, checking out master")
++ checkout_repo_branch(repo, "master")
++ end
++ end
++
++ elseif #branches > 0 and LUA_API_VER > branches[#branches] then
++ log.msg(log.info, "no newer branches, staying on master")
++ -- stay on master
++
++ else
++ -- checkout the appropriate branch for API version if it exists
++ log.msg(log.info, "checking out the appropriate API branch")
++
++ local match = false
++
++ for _x, branch in ipairs(branches) do
++ log.msg(log.debug, "checking branch " .. branch .. " against API " .. LUA_API_VER)
++
++ if LUA_API_VER == branch then
++ match = true
++ log.msg(log.info, "checking out repo branch " .. branch)
++ checkout_repo_branch(repo, branch)
++ log.msg(log.screen, _("you must restart darktable to use the correct version of the lua scripts"))
++ return
++ end
++
++ end
++
++ if not match then
++ log.msg(log.warn, "no matching branch found for " .. LUA_API_VER)
++ end
++
++ end
++ end
++ end
++end
++
++scan_scripts(LUA_DIR)
++log.msg(log.info, "finished processing scripts")
++
++if system_based then
++ -- exec user luarc file if it exists
++ local luarc = loadfile(USER_LUA_DIR .. "rc")
++ if luarc then
++ luarc()
++ end
++
++ if df.test_file(USER_LUA_DIR, "d") then
++ LUA_DIR = USER_LUA_DIR
++ scan_scripts(LUA_DIR)
++ LUA_DIR = SYSTEM_LUA_DIR
++ end
++
++ dt.configuration["lua_dir"] = LUA_DIR
++
++ dt.print_log("lua dir is " .. dt.configuration.lua_dir)
++ -- scan_script(USER_LUA_DIR)
++end
++
++
++-- - - - - - - - - - - - - - - - - - - - - - - -
++-- U S E R I N T E R F A C E
++-- - - - - - - - - - - - - - - - - - - - - - - -
++
++-- update the scripts
++
++sm.widgets.update_script_choices = dt.new_widget("combobox"){
++ label = _("scripts to update"),
++ tooltip = _("select the scripts installation to update"),
++ selected = 1,
++ changed_callback = function(this)
++ pref_write("update_script_choices", "integer", this.selected)
++ end,
++ "placeholder",
++}
++
++sm.widgets.update = dt.new_widget("button"){
++ label = _("update scripts"),
++ tooltip = _("update the lua scripts from the repository"),
++ clicked_callback = function(this)
++ update_scripts()
++ end
++}
++
++-- add additional scripts
++
++sm.widgets.script_url = dt.new_widget("entry"){
++ text = "",
++ placeholder = "https://<repository url>",
++ tooltip = _("enter the URL of the git repository containing the scripts you wish to add")
++}
++
++sm.widgets.new_folder = dt.new_widget("entry"){
++ text = "",
++ placeholder = _("name of new folder"),
++ tooltip = _("enter a folder name for the additional scripts")
++}
++
++sm.widgets.add_scripts = dt.new_widget("box"){
++ orientation = vertical,
++ dt.new_widget("label"){label = _("URL to download additional scripts from")},
++ sm.widgets.script_url,
++ dt.new_widget("label"){label = _("new folder to place scripts in")},
++ sm.widgets.new_folder,
++ dt.new_widget("button"){
++ label = _("install additional scripts"),
++ clicked_callback = function(this)
++ install_scripts()
++ end
++ }
++}
++
++sm.widgets.allow_disable = dt.new_widget("check_button"){
++ label = _('enable "disable scripts" button'),
++ value = false,
++ clicked_callback = function(this)
++ if this.value == true then
++ sm.widgets.disable_scripts.sensitive = true
++ else
++ sm.widgets.disable_scripts.sensitive = false
++ end
++ end,
++}
++
++sm.widgets.disable_scripts = dt.new_widget("button"){
++ label = _("disable scripts"),
++ sensitive = false,
++ clicked_callback = function(this)
++ local LUARC = dt.configuration.config_dir .. PS .. "luarc"
++ df.file_move(LUARC, LUARC .. ".disabled")
++ log.msg(log.info, "lua scripts disabled")
++ log.msg(log.screen, _("lua scripts will not run the next time darktable is started"))
++ end
++}
++
++sm.widgets.install_update = dt.new_widget("box"){
++ orientation = "vertical",
++ dt.new_widget("section_label"){label = " "},
++ dt.new_widget("label"){label = " "},
++ dt.new_widget("label"){label = _("update scripts")},
++ dt.new_widget("label"){label = " "},
++ sm.widgets.update_script_choices,
++ sm.widgets.update,
++ dt.new_widget("section_label"){label = " "},
++ dt.new_widget("label"){label = " "},
++ dt.new_widget("label"){label = _("add more scripts")},
++ dt.new_widget("label"){label = " "},
++ sm.widgets.add_scripts,
++ dt.new_widget("section_label"){label = " "},
++ dt.new_widget("label"){label = " "},
++ dt.new_widget("label"){label = _("disable scripts")},
++ dt.new_widget("label"){label = " "},
++ sm.widgets.allow_disable,
++ sm.widgets.disable_scripts,
++ dt.new_widget("label"){label = " "},
++}
++
++-- manage the scripts
++
++sm.widgets.folder_selector = dt.new_widget("combobox"){
++ label = _("folder"),
++ tooltip = _( "select the script folder"),
++ selected = 1,
++ changed_callback = function(self)
++ if sm.run then
++ pref_write("folder_selector", "integer", self.selected)
++ change_folder(sm.folders[self.selected])
++ end
++ end,
++ table.unpack(sm.translated_folders),
++}
++
++-- a script "button" consists of:
++-- a button to start and stop the script
++-- a label that contains the name of the script
++-- a horizontal box that contains the button and the label
++
++sm.widgets.buttons ={}
++sm.widgets.labels = {}
++sm.widgets.boxes = {}
++
++for i =1, DEFAULT_BUTTONS_PER_PAGE do
++ table.insert(sm.widgets.buttons, dt.new_widget("button"){})
++ table.insert(sm.widgets.labels, dt.new_widget("label"){})
++ table.insert(sm.widgets.boxes, dt.new_widget("box"){ orientation = "horizontal", expand = false, fill = false,
++ sm.widgets.buttons[i], sm.widgets.labels[i]})
++ sm.page_status.buttons_created = sm.page_status.buttons_created + 1
++end
++
++local page_back = "<"
++local page_forward = ">"
++
++sm.widgets.page_status = dt.new_widget("label"){label = _("page") .. ":"}
++
++sm.widgets.page_back = dt.new_widget("button"){
++ label = page_back,
++ clicked_callback = function(this)
++ if sm.run then
++ paginate(0)
++ end
++ end
++}
++
++sm.widgets.page_forward = dt.new_widget("button"){
++ label = page_forward,
++ clicked_callback = function(this)
++ if sm.run then
++ paginate(1)
++ end
++ end
++}
++
++sm.widgets.page_control = dt.new_widget("box"){
++ orientation = "horizontal",
++ sm.widgets.page_back,
++ sm.widgets.page_status,
++ sm.widgets.page_forward,
++}
++
++sm.widgets.scripts = dt.new_widget("box"){
++ orientation = vertical,
++ dt.new_widget("section_label"){label = " "},
++ dt.new_widget("label"){label = " "},
++ dt.new_widget("label"){label = _("scripts")},
++ sm.widgets.folder_selector,
++ sm.widgets.page_control,
++ table.unpack(sm.widgets.boxes),
++}
++
++-- configure options
++
++sm.widgets.num_buttons = dt.new_widget("slider"){
++ label = _("scripts per page"),
++ tooltip = _("select number of start/stop buttons to display"),
++ soft_min = MIN_BUTTONS_PER_PAGE,
++ hard_min = MIN_BUTTONS_PER_PAGE,
++ soft_max = MAX_BUTTONS_PER_PAGE,
++ hard_max = MAX_BUTTONS_PER_PAGE,
++ step = 1,
++ digits = 0,
++ value = 10
++}
++
++sm.widgets.change_buttons = dt.new_widget("button"){
++ label = _("change number of buttons"),
++ clicked_callback = function(this)
++ change_num_buttons()
++ end
++}
++
++sm.widgets.configure = dt.new_widget("box"){
++ orientation = "vertical",
++ dt.new_widget("section_label"){label = " "},
++ dt.new_widget("label"){label = " "},
++ dt.new_widget("label"){label = _("configuration")},
++ dt.new_widget("label"){label = " "},
++ sm.widgets.num_buttons,
++ dt.new_widget("label"){label = " "},
++ sm.widgets.change_buttons,
++ dt.new_widget("label"){label = " "},
++}
++
++-- stack for the options
++
++sm.widgets.main_stack = dt.new_widget("stack"){
++ sm.widgets.install_update,
++ sm.widgets.configure,
++ sm.widgets.scripts,
++}
++
++sm.widgets.main_stack.h_size_fixed = false
++sm.widgets.main_stack.v_size_fixed = false
++
++-- main menu
++
++sm.widgets.main_menu = dt.new_widget("combobox"){
++ label = _("action"),
++ changed_callback = function(self)
++ sm.widgets.main_stack.active = self.selected
++ pref_write("main_menu_action", "integer", self.selected)
++ log.msg(log.debug, "saved " .. self.selected .. " for main menu")
++ end,
++ _("install/update scripts"), _("configure"), _("start/stop scripts")
++}
++
++-- widget for module
++
++sm.widgets.main_box = dt.new_widget("box"){
++ sm.widgets.main_menu,
++ sm.widgets.main_stack
++}
++
++script_manager_running_script = nil
++
++-- - - - - - - - - - - - - - - - - - - - - - - -
++-- D A R K T A B L E I N T E G R A T I O N
++-- - - - - - - - - - - - - - - - - - - - - - - -
++
++if (dt.gui.current_view().id == "lighttable") and darktable_gui_safe then
++ install_module()
++else
++ if not sm.event_registered then
++ dt.register_event(
++ "script_manager", "view-changed",
++ function(event, old_view, new_view)
++ if new_view.name == "lighttable" and old_view.name == "darkroom" then
++ install_module()
++ end
++ end
++ )
++ sm.event_registered = true
++ end
++end
++
diff --git a/sources b/sources
index 760c9cd..6c44a91 100644
--- a/sources
+++ b/sources
@@ -1 +1 @@
-SHA512 (darktable-5.4.1.tar.xz) = d956fdac2d7a1782a242c8e0839a6d26fb3a2a7d90716978227b1c884f4acf0f51e599a72b0919f7b7f2ea2b5df40fecd56dfa8e7c813587ef469ff6e42ffe7b
+SHA512 (darktable-5.6.1.tar.xz) = bb4e1d27cb8781cef4dfd5cb2a580f4c88739a23e285675b5ca15f010aefffc965499cf8957880361e33616ec49461d149126af7ef1c9818322ae8f47df78d9e
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-09-01 8:46 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-01 8:46 [rpms/darktable] rawhide: 5.6.1 release Germano Massullo
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox