summary history branches tags files
darktable/photo-what-what.lua
--[[
   photo-what-what.lua - pww plugin for Darktable

   Copyright (C) 2025 Trevor Bentley <pww@x.mrmekon.com>.

   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 <http://www.gnu.org/licenses/>.
]]
--[[
   photo-what-what - use pww to automatically tag selected image(s)

   This launches pww, a utility for automatic image tagging, which
   analyzes the image and attaches new tags based on its content.
   These new tags are then imported into Darktable.

   ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
   * photo-what-what
   * libexiv2
   * libgexiv2
   * libopenimageio
   * any application that outputs a list of image tags

   USAGE
   * install & configure pww so it works with `photo-what-what <img>`
     - or change default paths in darktable settings
   * copy this script into darktable's lua/contrib/ directory
   * require this script in darktable's luarc config file
   * select one or more images
   * click the "auto-tag images (pww)" button

   BUGS, COMMENTS, SUGGESTIONS
   * Send to Trevor Bentley: pww@x.mrmekon.com

   CHANGES
   * 2025-01-13: initial implementation
]]

local dt = require "darktable"
local du = require "lib/dtutils"
local dlog = require "lib/dtutils.log"
local dsys = require 'lib/dtutils.system'

local namespace <const> = "photo-what-what"
local LOG_LEVEL <const> = dlog.info

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 = _("photo-what-what"),
  purpose = _("automatically tag image(s) with photo-what-what analysis tool"),
  author = "Trevor Bentley <pww@x.mmekon.com>",
  help = ""
}

script_data.destroy = nil
script_data.destroy_method = nil
script_data.restart = nil
script_data.show = nil

du.check_min_api_version("9.0.0", "photo-what-what")

local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"

local settings = {
   pww_bin_path = {},
   pww_script_path = {},
}

local function default_to(value, default)
    if value == 0 or value == "" then
        return default
    end
    return value
end

local function load_preferences()
   settings.pww_bin_path = default_to(dt.preferences.read(namespace, "pww_bin_path", "string"), "photo-what-what")
   settings.pww_script_path = default_to(dt.preferences.read(namespace, "pww_script_path", "string"), "")
end


local job
local function stop_job(job)
   if job then
      job.valid = false
   end
end

local function call_pww(images)
   load_preferences()
   local selection = dt.gui.selection()

   -- process each selected image
   job = dt.gui.create_job(_("pww auto-tagging"), true, stop_job)
   for idx, image in ipairs(images) do
      if not job.valid then
         break
      end
      job.percent = ((idx-1) / #images)

      -- I can't believe DT doesn't do this for us
      local file = image.path .. PS .. image.filename

      -- build up command from strings.  yucky.
      local cmd = settings.pww_bin_path
      cmd = string.format("%s", cmd)
      if settings.pww_script_path ~= "" then
         cmd = string.format("%s -b \"%s\"", cmd, settings.pww_script_path)
      end
      cmd = string.format("%s \"%s\"", cmd, file)

      -- run pww
      local resp = dsys.external_command(cmd)
      if resp ~= 0 then
         dt.print("error running photo-what-what")
         if job.valid then
            job.valid = false
         end
         break
      end

      -- re-import file to read the updated metadata, since it was changed outside of darktable.
      -- This does not change the image ID.
      dt.database.import(file)
      -- re-select the same selection, since importing clears it
      dt.gui.selection(selection)
   end

   if job.valid then
      job.valid = false
   end
end

local function destroy()
  dt.destroy_event("photo-what-what", "shortcut")
  dt.gui.libs.image.destroy_action("photo-what-what")
end

script_data.destroy = destroy

load_preferences()

-- register preferences in darktable settings
dt.preferences.register(namespace, "pww_script_path", "string", "pww script path", "Full path to identifier application/script used by pww (optional)", "")
dt.preferences.register(namespace, "pww_bin_path", "string", "pww bin path", "Full path to the photo-what-what executable", "photo-what-what")

-- register button in action menu
dt.gui.libs.image.register_action(
   namespace, _("auto-tag images (pww)"),
   function(event, images) call_pww(images) end,
   _("auto-tag image(s) with photo-what-what analysis tool")
)

-- register keyboard shortcut handler
dt.register_event(
   namespace, "shortcut",
   function(event, shortcut) call_pww(dt.gui.action_images) end,
   _("auto-tag image(s) with photo-what-what analysis tool")
)

return script_data