diff --git a/official/select_duplicates.lua b/official/select_duplicates.lua new file mode 100644 index 00000000..4bec9ce5 --- /dev/null +++ b/official/select_duplicates.lua @@ -0,0 +1,266 @@ +--[[ + + select_duplicates.lua - select duplicate images from a collection + + Copyright (C) 2026 Bill Ferguson . + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +]] +--[[ + select_duplicates - select duplicate images from a collection + + select_duplicates adds a button to the select module to select + duplicate images from a collection. If the selection2collection + script is also active an option is available to have the selected + duplicates appear as a temporary collection. + + A shortcut is available to select the duplicates from the collection + but it is somewhat slower because it doesn't have access to the current + image cache and therfore has to open each image separately. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + + None + + USAGE + + * enable in script manager + * Press the button + + BUGS, COMMENTS, SUGGESTIONS + Bill Ferguson + + CHANGES +]] + +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 debug = require "darktable.debug" + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("7.0.0", MODULE) -- choose the minimum version that contains the features you need + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- I 1 8 N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- S C R I P T M A N A G E R I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local script_data = {} + +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 +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 + +script_data.metadata = { + name = _("select duplicates"), -- visible name of script + purpose = _("select duplicate images from a collection"), -- purpose of script + author = "Bill Ferguson ", -- your name and optionally e-mail address + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/select_duplicates/" -- URL to help/documentation +} + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "select_duplicates" +local DEFAULT_LOG_LEVEL = log.info +local TMP_DIR = dt.configuration.tmp_dir + +-- path separator +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +-- command separator +local CS = dt.configuration.running_os == "windows" and "&" or ";" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- L O G L E V E L +-- - - - - - - - - - - - - - - - - - - - - - - - + +log.log_level(DEFAULT_LOG_LEVEL) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local select_duplicates = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- G L O B A L V A R I A B L E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +select_duplicates.log_level = DEFAULT_LOG_LEVEL +select_duplicates.create_collection = false + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- P R E F E R E N C E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.preferences.register(MODULE, "create_collection", "bool", + script_data.metadata.name .. _(" create collection from selection"), + _("create collection of selected duplicates"), false) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A L I A S E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local namespace = select_duplicates +local sd = select_duplicates + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +------------------- +-- helper functions +------------------- + +local function set_log_level(level) + if not level then + level = namespace.log_level + end + + local old_log_level = log.log_level() + log.log_level(level) + + return old_log_level +end + +local function restore_log_level(level) + if not level then + level = namespace.log_level + end + log.log_level(level) +end + +local function reset_log_level() + log.log_level(DEFAULT_LOG_LEVEL) + namespace.log_level = DEFAULT_LOG_LEVEL +end + +local function pref_read(name, pref_type) + local old_log_level = set_log_level(namespace.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(namespace.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 + +local function select_duplicate_files(event, images) + local duplicates = {} + local sorted_files = {} + sd.create_collection = pref_read("create_collection", "bool") + + for _, img in ipairs(images) do + local rawname = img.path .. PS .. img.filename + if not sorted_files[rawname] then + sorted_files[rawname] = {} + end + table.insert(sorted_files[rawname], img) + end + for k,v in pairs(sorted_files) do + if #v > 1 then + local old_image = nil + for _,i in ipairs(v) do + if not old_image then + old_image = i + else + if i.duplicate_index > old_image.duplicate_index then + table.insert(duplicates, i) + else + table.insert(duplicates, old_image) + old_image = i + end + end + end + end + end + if sd.create_collection then + dt.util.message(MODULE, "selection2collection", "create") + end + return duplicates +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N P R O G R A M +-- - - - - - - - - - - - - - - - - - - - - - - - + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- U S E R I N T E R F A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.gui.libs.select.register_selection(MODULE, _("select duplicates"), select_duplicate_files, + _("select duplicated images")) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + dt.gui.libs.select.destroy_selection(MODULE) +end + +script_data.destroy = destroy + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +if not dt.query_event(MODULE, "shortcut") then -- skip if shortcut already registered + dt.register_event(MODULE, "shortcut", + function(event, shortcut) + local duplicates = select_duplicate_files(event, dt.collection) + if duplicates then + dt.gui.selection(duplicates) + end + end, _("select duplicates") + ) +end + +return script_data