diff --git a/README.md b/README.md
index 49551e5..2381dbc 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,7 @@ LabelsToTags|Yes|LMW|Apply tags based on color labels and ratings
OpenInExplorer|No|LMW|Open the selected images in the system file manager
passport_guide|Yes|LMW|Add passport cropping guide to darkroom crop tool
pdf_slideshow|No|LM|Export images to a PDF slideshow
+[photils](https://github.com/scheckmedia/photils-dt)|No|LM|Automatic tag suggestions for your images
quicktag|Yes|LMW|Create shortcuts for quickly applying tags
rate_group|Yes|LMW|Apply or remove a star rating from grouped images
rename-tags|Yes|LMW|Change a tag name
diff --git a/contrib/photils.lua b/contrib/photils.lua
new file mode 100644
index 0000000..102bd4d
--- /dev/null
+++ b/contrib/photils.lua
@@ -0,0 +1,416 @@
+--[[ photils Auto Tagging plugin
+ copyright (c) 2020 Tobias Scheck
+
+ 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 .
+--]]
+
+--[[
+ A darktable plugin which tries to predict keywords based on the selected image.
+ This plugin uses photils-cli to handle this task. Photils-cli is an application
+ which passes the image through a neural network and generates a feature vector.
+ This feature vector will be used to find similar images in a self-hosted online
+ database and the corresponding tags. The usage of the feature vector has this advantage
+ that at no time your selected image will leave the PC. Rather a representation
+ of the image is sent.
+
+ ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
+ * photils-cli - https://github.com/scheckmedia/photils-cli
+
+ USAGE
+ * require this script from your main lua file
+ To do this add this line to the file .config/darktable/luarc:
+ require "contrib/photils"
+ * Select an image
+ * Press "Get Tags"
+ * Select the tags you want from a list of suggestions
+ * Press "Attach .. Tags" to add the selected tags to your image
+--]]
+
+local dt = require "darktable"
+local du = require "lib/dtutils"
+local df = require "lib/dtutils.file"
+local dtsys = require "lib/dtutils.system"
+
+local MODULE_NAME = "photils"
+du.check_min_api_version("5.0.0", MODULE_NAME)
+
+local PS = dt.configuration.running_os == "windows" and "\\" or "/"
+local gettext = dt.gettext
+gettext.bindtextdomain(MODULE_NAME,
+ dt.configuration.config_dir .. PS .. "lua" .. PS .. "locale" .. PS)
+
+local exporter = dt.new_format("jpeg")
+exporter.quality = 80
+exporter.max_height = 224
+exporter.max_width = 224
+
+-- helper functions
+
+local function _(msgid)
+ return gettext.dgettext(MODULE_NAME, msgid)
+end
+
+local function num_keys(tbl)
+ local num = 0
+ for _ in pairs(tbl) do num = num + 1 end
+ return num
+end
+
+local function has_key(tbl, value)
+ for k, _ in pairs(tbl) do
+ if k == value then
+ return true
+ end
+ end
+
+ return false
+end
+
+local photils_installed = df.check_if_bin_exists("photils-cli")
+
+--[[
+ local state object
+
+ maybe per_page is a preference variable but I think 10
+ is a good value for the ui
+]]
+local PHOTILS = {
+ tags = {},
+ page = 1,
+ per_page = 10,
+ selected_tags = {},
+ in_pagination = false,
+ tagged_image = ""
+}
+
+local GUI = {
+ container = dt.new_widget("box") {
+ orientation = "vertical",
+ sensitive = true,
+ dt.new_widget("button") {
+ label = _("Get Tags"),
+ sensitive = photils_installed,
+ clicked_callback = function() PHOTILS.on_tags_clicked() end
+ },
+ reset_callback = function() PHOTILS.on_reset(true) end
+ },
+ stack = dt.new_widget("stack"),
+ prev_button = dt.new_widget("button") {
+ label = "<",
+ sensitive = false,
+ clicked_callback = function()
+ PHOTILS.page = PHOTILS.page - 1
+ PHOTILS.paginate()
+ end
+ },
+ next_button = dt.new_widget("button") {
+ label = ">",
+ sensitive = false,
+ clicked_callback = function()
+ PHOTILS.page = PHOTILS.page + 1
+ PHOTILS.paginate()
+ end
+ },
+ tag_box = dt.new_widget("box") {orientation = "vertical"},
+ tag_view = dt.new_widget("box") {orientation = "vertical"},
+ page_label = dt.new_widget("label") {label = ""},
+ error_view = dt.new_widget("box") {orientation = "vertical"},
+ warning_label = dt.new_widget("label") {
+ label = ""
+ },
+ restart_required_label = dt.new_widget("label") {
+ label = _("requires a restart to be applied")
+ },
+ attach_button = dt.new_widget("button") {
+ label = "",
+ sensitive = false,
+ clicked_callback = function() PHOTILS.attach_tags() end
+ },
+ confidence_slider = dt.new_widget("slider") {
+ step = 1,
+ digits = 0,
+ value = 90,
+ hard_max = 100,
+ hard_min = 0,
+ soft_max = 100,
+ soft_min = 0,
+ label = _("Min Confidence Value")
+ },
+ warning = dt.new_widget("label")
+}
+
+function PHOTILS.image_changed()
+ local current_image = tostring(dt.gui.selection()[1])
+ if current_image ~= PHOTILS.tagged_image then
+ if PHOTILS.tagged_image ~= "" then
+ PHOTILS.tagged_image_has_changed()
+ end
+
+ PHOTILS.tagged_image = tostring(current_image)
+ end
+end
+
+function PHOTILS.tagged_image_has_changed()
+ GUI.warning.label = _(
+ "The suggested tags were not generated\n for the currently selected image!")
+end
+
+function PHOTILS.paginate()
+ PHOTILS.in_pagination = true
+ local num_pages = math.ceil(#PHOTILS.tags / PHOTILS.per_page)
+ GUI.page_label.label = string.format(_(" Page %s of %s "), PHOTILS.page,
+ num_pages)
+
+ if PHOTILS.page <= 1 then
+ PHOTILS.page = 1
+ GUI.prev_button.sensitive = false
+ else
+ GUI.prev_button.sensitive = true
+ end
+
+ if PHOTILS.page > num_pages - 1 then
+ PHOTILS.page = num_pages
+ GUI.next_button.sensitive = false
+ else
+ GUI.next_button.sensitive = true
+ end
+
+ local offset = ((PHOTILS.page - 1) * PHOTILS.per_page) + 1
+ local tag_index = 1
+ for i = offset, offset + PHOTILS.per_page - 1, 1 do
+ local tag = PHOTILS.tags[i]
+ GUI.tag_box[tag_index].value = has_key(PHOTILS.selected_tags, tag)
+
+ if tag then
+ GUI.tag_box[tag_index].label = tag
+ GUI.tag_box[tag_index].sensitive = true
+ else
+ GUI.tag_box[tag_index].label = ""
+ GUI.tag_box[tag_index].sensitive = false
+ end
+ tag_index = tag_index + 1
+ end
+
+ PHOTILS.in_pagination = false
+end
+
+function PHOTILS.attach_tags()
+ local image = dt.gui.selection()[1]
+ for tag, _ in pairs(PHOTILS.selected_tags) do
+ local dt_tag = dt.tags.create(tag)
+ dt.tags.attach(dt_tag, image)
+ end
+
+ dt.print(_("Tags successfully attached to image"))
+end
+
+function PHOTILS.get_tmp_file()
+ local tmp_file = os.tmpname()
+ if dt.configuration.running_os == "windows" then
+ tmp_file = dt.configuration.tmp_dir .. tmp_file -- windows os.tmpname() defaults to root directory
+ end
+
+ local f = io.open(tmp_file, "w")
+ if not f then
+ dt.print_log(string.format(_("Error writing to `%s`"), tmp_file))
+ os.remove(tmp_file)
+ return nil
+ end
+
+ return tmp_file
+end
+
+function PHOTILS.get_tags(image, with_export)
+ local tmp_file = PHOTILS.get_tmp_file()
+ local in_arg = df.sanitize_filename(tostring(image))
+ local out_arg = df.sanitize_filename(tmp_file)
+ local executable = photils_installed
+
+ if dt.configuration.running_os == "macos" then
+ executable = executable .. "/Contents/MacOS/photils-cli"
+ end
+
+ if with_export then
+ dt.print_log("use export to for prediction")
+ local export_file = PHOTILS.get_tmp_file()
+ exporter:write_image(image, export_file)
+ in_arg = df.sanitize_filename(tostring(export_file))
+ end
+
+ local command = executable .. " -c " .. " -i " .. in_arg .. " -o " .. out_arg
+
+ local ret = dtsys.external_command(command)
+ if ret > 0 then
+ dt.print_error(string.format("command %s returned error code %d", command, ret))
+ os.remove(tmp_file)
+
+ -- try to export the image and run tagging
+ if not with_export then
+ return PHOTILS.get_tags(image, true)
+ end
+
+ return false
+ end
+
+ for i = #PHOTILS.tags, 1, -1 do
+ PHOTILS.tags[i] = nil
+ end
+
+ for tag in io.lines(tmp_file) do
+ local splitted = du.split(tag, ":")
+ if 100 * tonumber(splitted[2]) >= GUI.confidence_slider.value then
+ PHOTILS.tags[#PHOTILS.tags + 1] = splitted[1]
+ end
+ end
+
+ dt.print(string.format(_("%s found %d tags for your image"), MODULE_NAME,
+ #PHOTILS.tags))
+ os.remove(tmp_file)
+
+ return true
+end
+
+function PHOTILS.on_tags_clicked()
+ PHOTILS.page = 1
+ GUI.warning.label = ""
+
+ PHOTILS.on_reset(false)
+
+ local images = dt.gui.selection()
+
+ if #images == 0 then
+ dt.print(_("No image selected."))
+ dt.control.sleep(2000)
+ else
+ if #images > 1 then
+ dt.print(_("This plugin can only handle a single image."))
+ dt.gui.selection({images[1]})
+ dt.control.sleep(2000)
+ end
+
+ if not PHOTILS.get_tags(images[1], true) then
+ local msg = string.format(_("%s failed, see terminal output for details"), MODULE_NAME)
+ GUI.warning_label.label = msg
+ GUI.stack.active = GUI.error_view
+ dt.print(msg)
+ return
+ end
+
+ if #PHOTILS.tags == 0 then
+ local msg = string.format(_("No tags where found"), MODULE_NAME)
+ GUI.warning_label.label = msg
+ GUI.stack.active = GUI.error_view
+ return
+ end
+
+ GUI.stack.active = GUI.tag_view
+ PHOTILS.paginate()
+ end
+end
+
+function PHOTILS.tag_selected(tag_button)
+ if PHOTILS.in_pagination then return end
+
+ if tag_button.value then
+ PHOTILS.selected_tags[tag_button.label] = tag_button.label
+ else
+ PHOTILS.selected_tags[tag_button.label] = nil
+ end
+
+ local num_selected = num_keys(PHOTILS.selected_tags)
+ if num_selected == 0 then
+ GUI.attach_button.label = ""
+ GUI.attach_button.sensitive = false
+ else
+ GUI.attach_button.label = string.format(_("Attach %d Tags"),
+ num_selected)
+ GUI.attach_button.sensitive = true
+ end
+end
+
+function PHOTILS.on_reset(with_view)
+ if with_view then GUI.stack.active = 1 end
+
+ for k, _ in pairs(PHOTILS.selected_tags) do
+ PHOTILS.selected_tags[k] = nil
+ end
+
+ for _, v in ipairs(GUI.tag_box) do
+ v.value = false
+ end
+
+ GUI.attach_button.label = ""
+ GUI.attach_button.sensitive = false
+end
+
+-- add a fix number of buttons
+for _ = 1, PHOTILS.per_page, 1 do
+ local btn_tag = dt.new_widget("check_button") {
+ label = "",
+ sensitive = false,
+ clicked_callback = PHOTILS.tag_selected
+ }
+
+ table.insert(GUI.tag_box, btn_tag)
+end
+
+if not photils_installed then
+ GUI.warning_label.label = _("photils-cli not found")
+ dt.print_log(_("photils-cli not found"))
+else
+ GUI.warning_label.label = _("Select an image, click \"Get Tags\" and get \nsuggestions for tags.")
+end
+
+GUI.pagination = dt.new_widget("box") {
+ orientation = "horizontal",
+ GUI.prev_button,
+ GUI.page_label,
+ GUI.next_button
+}
+
+
+table.insert(GUI.error_view, GUI.warning_label)
+if not photils_installed then
+ table.insert(GUI.error_view, df.executable_path_widget({"photils-cli"}))
+ table.insert(GUI.error_view, GUI.restart_required_label)
+end
+table.insert(GUI.stack, GUI.error_view)
+table.insert(GUI.stack, GUI.tag_view)
+
+table.insert(GUI.tag_view, GUI.pagination)
+table.insert(GUI.tag_view, GUI.tag_box)
+table.insert(GUI.tag_view, GUI.attach_button)
+table.insert(GUI.tag_view, GUI.warning)
+
+table.insert(GUI.container, GUI.confidence_slider)
+table.insert(GUI.container, GUI.stack)
+
+GUI.stack.active = 1
+
+local plugin_display_views = {
+ [dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100},
+ [dt.gui.views.darkroom] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 100}
+}
+
+-- dt.control.dispatch(PHOTILS.image_changed)
+dt.register_event("mouse-over-image-changed",PHOTILS.image_changed)
+dt.register_lib(MODULE_NAME,
+ "photils autotagger",
+ true,
+ true,
+ plugin_display_views,
+ GUI.container,
+ nil,
+ nil
+)
diff --git a/locale/de_DE/LC_MESSAGES/photils.po b/locale/de_DE/LC_MESSAGES/photils.po
new file mode 100644
index 0000000..f1480a1
--- /dev/null
+++ b/locale/de_DE/LC_MESSAGES/photils.po
@@ -0,0 +1,83 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Lua photils\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-05-12 13:12+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../../../contrib/photils.lua:97
+msgid "Get Tags"
+msgstr "Hole Stichwörter"
+
+#: ../../../contrib/photils.lua:128
+msgid "requires a restart to be applied"
+msgstr "erfordert die Neustart um angewendet zu werden"
+
+#: ../../../contrib/photils.lua:155
+msgid ""
+"The suggested tags were not generated\n"
+" for the currently selected image!"
+msgstr ""
+"Die vorgeschlagenen Stichwörter wurden für das\n"
+"aktuell ausgewählte Bild nicht generiert!"
+
+#: ../../../contrib/photils.lua:161
+#, lua-format
+msgid " Page %s of %s "
+msgstr " Seite %s von %s "
+
+#: ../../../contrib/photils.lua:204
+msgid "Tags successfully attached to image"
+msgstr "Stichwörter erfolgreich an Bild angefügt"
+
+#: ../../../contrib/photils.lua:215
+#, lua-format
+msgid "Error writing to `%s`"
+msgstr "Fehler beim Schreiben in `%s`"
+
+#: ../../../contrib/photils.lua:229
+#, lua-format
+msgid "%s failed, see terminal output for details"
+msgstr "%s fehlgeschlagen, siehe Terminal-Ausgabe für Details"
+
+#: ../../../contrib/photils.lua:243
+#, lua-format
+msgid "%s found %d tags for your image"
+msgstr "%s gefunden %d-Stichwörter für Ihr Bild"
+
+#: ../../../contrib/photils.lua:257
+msgid "No image selected."
+msgstr "Kein Bild ausgewählt."
+
+#: ../../../contrib/photils.lua:262
+msgid "This plugin can only handle a single image."
+msgstr "Dieses Plugin kann nur ein einziges Bild verarbeiten."
+
+#: ../../../contrib/photils.lua:293
+#, lua-format
+msgid "Attach %d Tags"
+msgstr "%d-Stichwörter anhängen"
+
+#: ../../../contrib/photils.lua:326 ../../../contrib/photils.lua:327
+msgid "photils-cli not found"
+msgstr "photils-cli nicht gefunden"
+
+#: ../../../contrib/photils.lua:329
+msgid ""
+"Select an image, click \"Get Tags\" and get \n"
+"suggestions for tags."
+msgstr ""
+"Wählen Sie ein Bild aus, drücken Sie \"Hole Stichwörter\"\n"
+" und erhalten Sie Vorschläge für Stichwörter."