diff --git a/official/apply_camera_style.lua b/official/apply_camera_style.lua new file mode 100644 index 00000000..a72c8217 --- /dev/null +++ b/official/apply_camera_style.lua @@ -0,0 +1,493 @@ +--[[ + + apply_camera_style.lua - apply camera style to matching images + + Copyright (C) 2024 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 . +]] +--[[ + apply_camera_style - apply darktable camera style to matching images + + apply a camera style corresponding to the camera used to + take the image to provide a starting point for editing that + is similar to the SOOC jpeg. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + none + + USAGE + start the script from script_manager + + 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" + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "apply_camera_style" +local DEFAULT_LOG_LEVEL = log.info +local TMP_DIR = dt.configuration.tmp_dir +local STYLE_PREFIX = "_l10n_darktable camera styles|" + +-- path separator +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +-- command separator +local CS = dt.configuration.running_os == "windows" and "&" or ";" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("9.4.0", MODULE) -- styles use filmic V7 which appeared in darktable 4.4 + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- 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 = "apply_camera_style", -- name of script + purpose = _("apply darktable camera style to matching images"), -- purpose of script + author = "Bill Ferguson ", -- your name and optionally e-mail address + help = "https://docs.darktable.org/lua/development/lua.scripts.manual/scripts/official/apply_camera_style/" -- URL to help/documentation +} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- L O G L E V E L +-- - - - - - - - - - - - - - - - - - - - - - - - + +log.log_level(DEFAULT_LOG_LEVEL) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local apply_camera_style = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- G L O B A L V A R I A B L E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +apply_camera_style.imported_images = {} +apply_camera_style.styles = {} +apply_camera_style.log_level = DEFAULT_LOG_LEVEL + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- P R E F E R E N C E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A L I A S E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local namespace = apply_camera_style +local acs = apply_camera_style + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- 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 + +------------------- +-- script functions +------------------- + +local function process_pattern(pattern) + + local log_level = set_log_level(acs.log_level) + + pattern = string.lower(pattern) + -- strip off series + pattern = string.gsub(pattern, " series$", "?") + -- match a character + if string.match(pattern, "?$") then + -- handle EOS R case + pattern = string.gsub(pattern, "?", ".?") + else + pattern = string.gsub(pattern, "?", ".") + end + -- escape dashes + pattern = string.gsub(pattern, "%-", "%%-") + -- until we end up with a set, I'll defer set processing, i.e. [...] + -- anchor the pattern to ensure we don't short match + pattern = "^" .. pattern .. "$" + + restore_log_level(log_level) + + return pattern +end + +local function process_set(pattern_set) + + local log_level = set_log_level(acs.log_level) + + local to_process = {} + local processed = {} + + local base, set, tail + + if string.match(pattern_set, "]$") then + base, set = string.match(pattern_set, "(.+)%[(.+)%]") + else + base, set, tail = string.match(pattern_set, "(.+)%[(.+)%](.+)") + end + + log.msg(log.debug, "base is " .. base .. " and set is " .. set) + + to_process = du.split(set, ",") + + for _, item in ipairs(to_process) do + local pat = base .. item + if tail then + pat = pat .. tail + end + table.insert(processed, process_pattern(pat)) + end + + restore_log_level(log_level) + + return processed +end + +local function get_camera_styles() + + local log_level = set_log_level(acs.log_level) + + -- separate the styles into + -- + -- acs.styles - + -- maker - + -- styles {} + -- patterns {} + + for _, style in ipairs(dt.styles) do + + if string.match(style.name, STYLE_PREFIX) then + log.msg(log.debug, "got " .. style.name) + + local parts = du.split(style.name, "|") + parts[2] = string.lower(parts[2]) + log.msg(log.debug, "maker is " .. parts[2]) + + if not acs.styles[parts[2]] then + acs.styles[parts[2]] = {} + acs.styles[parts[2]]["styles"] = {} + acs.styles[parts[2]]["patterns"] = {} + end + if parts[3] then + if not string.match(parts[3], "]") then + table.insert(acs.styles[parts[2]].styles, style) + local processed_pattern = process_pattern(parts[#parts]) + table.insert(acs.styles[parts[2]].patterns, processed_pattern) + log.msg(log.debug, "pattern for " .. style.name .. " is " .. processed_pattern) + else + local processed_patterns = process_set(parts[3]) + for _, pat in ipairs(processed_patterns) do + table.insert(acs.styles[parts[2]].styles, style) + table.insert(acs.styles[parts[2]].patterns, pat) + log.msg(log.debug, "pattern for " .. style.name .. " is " .. pat) + end + end + end + end + end + + restore_log_level(log_level) + +end + +local function normalize_model(maker, model) + + local log_level = set_log_level(acs.log_level) + + model = string.lower(model) + + -- strip off the maker name + if maker == "canon" then + model = string.gsub(model, "canon ", "") + elseif maker == "hasselblad" then + model = string.gsub(model, "hasselblad ", "") + elseif maker == "leica" then + model = string.gsub(model, "leica ", "") + elseif maker == "lg" then + model = string.gsub(model, "lg ", "") + elseif maker == "nikon" then + model = string.gsub(model, "nikon ", "") + elseif maker == "nokia" then + model = string.gsub(model, "nokia ", "") + elseif maker == "oneplus" then + model = string.gsub(model, "oneplus ", "") + elseif maker == "pentax" then + model = string.gsub(model, "pentax ", "") + model = string.gsub(model, "ricoh ", "") + end + + restore_log_level(log_level) + + return model +end + +local function normalize_maker(maker) + + local log_level = set_log_level(acs.log_level) + + maker = string.lower(maker) + + if string.match(maker, "^fujifilm") then + maker = "fujifilm" + elseif string.match(maker, "^hmd ") then + maker = "nokia" + elseif string.match(maker, "^leica") then + maker = "leica" + elseif string.match(maker, "^minolta") then + maker = "minolta" + elseif string.match(maker, "^nikon") then + maker = "nikon" + elseif string.match(maker, "^om ") then + maker = "om system" + elseif string.match(maker, "^olympus") then + maker = "olympus" + elseif string.match(maker, "^pentax") or string.match(maker, "^ricoh") then + maker = "pentax" + end + + restore_log_level(log_level) + + return maker +end + +local function has_style_tag(image, tag_name) + + local log_level = set_log_level(acs.log_level) + + local result = false + + log.msg(log.debug, "looking for tag " .. tag_name) + + for _, tag in ipairs(image:get_tags()) do + log.msg(log.debug, "checking against " .. tag.name) + if tag.name == tag_name then + log.msg(log.debug, "matched tag " .. tag_name) + result = true + end + end + + restore_log_level(log_level) + + return result +end + +local function mangle_model(model) + + local log_level = set_log_level(acs.log_level) + + if string.match(model, "eos") then + log.msg(log.debug, "mangle model got " .. model) + model = string.gsub(model, "r6m2", "r6 mark ii") + model = string.gsub(model, "eos 350d digital", "eos kiss digital n") + model = string.gsub(model, "eos 500d", "eos rebel t1") + model = string.gsub(model, "eos 550d", "eos rebel t2") + model = string.gsub(model, "eos 600d", "eos rebel t3i") + model = string.gsub(model, "eos 650d", "eos rebel t4i") + model = string.gsub(model, "eos 700d", "eos rebel t5") + model = string.gsub(model, "eos 750d", "eos rebel t6i") + model = string.gsub(model, "eos 760d", "eos rebel t6s") + model = string.gsub(model, "eos 100d", "eos rebel t6") + model = string.gsub(model, "eos 1100d", "eos rebel t3") + model = string.gsub(model, "eos 1200d", "eos rebel t5") + model = string.gsub(model, "eos 1300d", "eos rebel t6") + model = string.gsub(model, "eos 2000d", "eos rebel t7") + log.msg(log.debug, "mandle model returning " .. model) + end + + restore_log_level(log_level) + + return model +end + +local function stop_job() + if acs.job then + if acs.job.valid then + acs.job.valid = false + end + end +end + +local function apply_style_to_images(images) + + local log_level = set_log_level(acs.log_level) + + acs.job = dt.gui.create_job(_("applying camera styles to images"), true, stop_job) + + for count, image in ipairs(images) do + local maker = normalize_maker(image.exif_maker) + local model = normalize_model(maker, image.exif_model) + model = mangle_model(model) + log.msg(log.debug, "got maker " .. maker .. " and model " .. model .. " from image " .. image.filename) + + if acs.styles[maker] then + local no_match = true + for i, pattern in ipairs(acs.styles[maker].patterns) do + if string.match(model, pattern) or + (i == #acs.styles[maker].patterns and string.match(pattern, "generic")) then + local tag_name = "darktable|style|" .. acs.styles[maker].styles[i].name + if not has_style_tag(image, tag_name) then + image:apply_style(acs.styles[maker].styles[i]) + no_match = false + log.msg(log.info, "applied style " .. acs.styles[maker].styles[i].name .. " to " .. image.filename) + end + log.log_level(loglevel) + break + end + end + if no_match then + log.msg(log.info, "no style found for " .. maker .. " " .. model) + end + else + log.msg(log.info, "no maker found for " .. image.filename) + end + if count % 10 == 0 then + acs.job.percent = count / #images + end + if dt.control.ending then + stop_job() + end + end + + stop_job() + + restore_log_level(log_level) + +end + +local function apply_camera_style(collection) + + local log_level = set_log_level(acs.log_level) + + local images = nil + + if collection == true then + images = dt.collection + log.msg(log.info, "applying camera styles to collection") + elseif collection == false then + images = dt.gui.selection() + if #images == 0 then + images = dt.gui.action_images + end + log.msg(log.info, "applying camera styles to selection") + end + apply_style_to_images(images) + + restore_log_level(log_level) + +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N P R O G R A M +-- - - - - - - - - - - - - - - - - - - - - - - - + +get_camera_styles() + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- U S E R I N T E R F A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + dt.destroy_event(MODULE, "post-import-image") + dt.destroy_event(MODULE, "post-import-film") +end + +script_data.destroy = destroy + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local shortcut_string = _("apply darktable camera styles to %s") + +dt.register_event(MODULE, "shortcut", + function(event, shortcut) + apply_camera_style(true) + end, string.format(shortcut_string, _("collection")) +) + +dt.register_event(MODULE, "shortcut", + function(event, shortcut) + apply_camera_style(false) + end, string.format(shortcut_string, _("selection")) +) + +dt.register_event(MODULE, "post-import-image", + function(event, image) + table.insert(acs.imported_images, image) + end +) + +dt.register_event(MODULE, "post-import-film", + function(event, film) + apply_style_to_images(acs.imported_images) + acs.imported_images = {} + end +) + +return script_data