From ad10542b3025a2a4e32d87cc3bd1ca61351a0fdb Mon Sep 17 00:00:00 2001 From: Bill Ferguson Date: Sun, 26 Mar 2023 14:55:40 -0400 Subject: [PATCH] lib/dtutils/string - Added variable substitution functions that are equivalent to the darktable variable substitution functions. --- lib/dtutils/string.lua | 411 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 411 insertions(+) diff --git a/lib/dtutils/string.lua b/lib/dtutils/string.lua index b988731..e315eda 100644 --- a/lib/dtutils/string.lua +++ b/lib/dtutils/string.lua @@ -476,6 +476,417 @@ function dtutils_string.get_filetype(str) return parts["filetype"] end +local substitutes = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local PLACEHOLDERS = {"ROLL.NAME", + "FILE.FOLDER", + "FILE.NAME", + "FILE.EXTENSION", + "ID", + "VERSION", + "VERSION.IF.MULTI", -- Not Implemented + "VERSION.NAME", + "DARKTABLE.VERSION", + "DARKTABLE.NAME", -- Not Implemented + "SEQUENCE", + "WIDTH.SENSOR", + "HEIGHT.SENSOR", + "WIDTH.RAW", + "HEIGHT.RAW", + "WIDTH.CROP", + "HEIGHT.CROP", + "WIDTH.EXPORT", + "HEIGHT.EXPORT", + "WIDTH.MAX", -- Not Implemented + "HEIGHT.MAX", -- Not Implemented + "YEAR", + "MONTH", + "DAY", + "HOUR", + "MINUTE", + "SECOND", + "MSEC", + "EXIF.YEAR", + "EXIF.MONTH", + "EXIF.DAY", + "EXIF.HOUR", + "EXIF.MINUTE", + "EXIF.SECOND", + "EXIF.MSEC", + "EXIF.DATE.REGIONAL", -- Not Implemented + "EXIF.TIME.REGIONAL", -- Not Implemented + "EXIF.ISO", + "EXIF.EXPOSURE", + "EXIF.EXPOSURE.BIAS", + "EXIF.APERTURE", + "EXIF.FOCAL.LENGTH", + "EXIF.FOCUS.DISTANCE", + "LONGITUDE", + "LATITUDE", + "ALTITUDE", + "STARS", + "RATING.ICONS", -- Not Implemented + "LABELS", + "LABELS.ICONS", -- Not Implemented + "MAKER", + "MODEL", + "TITLE", + "DESCRIPTION", + "CREATOR", + "PUBLISHER", + "RIGHTS", + "TAGS", -- Not Implemented + "CATEGORY", -- Not Implemented + "SIDECAR.TXT", -- Not Implemented + "FOLDER.PICTURES", + "FOLDER.HOME", + "FOLDER.DESKTOP", + "OPENCL.ACTIVATED", -- Not Implemented + "USERNAME", + "NL", + "JOBCODE" -- Not Implemented +} + +local PS = dt.configuration.running_os == "windows" and "\\" or "/" +local USER = os.getenv("USERNAME") +local HOME = dt.configuration.running_os == "windows" and os.getenv("HOMEPATH") or os.getenv("HOME") +local PICTURES = HOME .. PS .. (dt.configuration.running_os == "windows" and "My Pictures" or "Pictures") +local DESKTOP = HOME .. PS .. "Desktop" + +local function get_colorlabels(image) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + local colorlabels = {} + if image.red then table.insert(colorlabels, "red") end + if image.yellow then table.insert(colorlabels, "yellow") end + if image.green then table.insert(colorlabels, "green") end + if image.blue then table.insert(colorlabels, "blue") end + if image.purple then table.insert(colorlabels, "purple") end + local labels = #colorlabels == 1 and colorlabels[1] or du.join(colorlabels, ",") + log.log_level(old_log_level) + return labels +end + +dtutils_string.libdoc.functions["build_substitution_list"] = { + Name = [[build_substitution_list]], + Synopsis = [[build a list of variable substitutions]], + Usage = [[local ds = require "lib/dtutils.string" + ds.build_substitution_list(image, sequence, [username], [pic_folder], [home], [desktop]) + image - dt_lua_image_t - the image being processed + sequence - integer - the sequence number of the image + [username] - string - optional - user name. Will be determined if not supplied + [pic_folder] - string - optional - pictures folder name. Will be determined if not supplied + [home] - string - optional - home directory. Will be determined if not supplied + [desktop] - string - optional - desktop directory. Will be determined if not supplied]], + Description = [[build_substitution_list populates variables with values from the arguments + and determined from the system and darktable.]], + Return_Value = [[]], + Limitations = [[If the value for a variable can not be determined, or if it is not supported, + then an empty string is used for the value.]], + Example = [[]], + See_Also = [[https://docs.darktable.org/usermanual/4.2/en/special-topics/variables/]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +function dtutils_string.build_substition_list(image, sequence, username, pic_folder, home, desktop) + -- build the argument substitution list from each image + + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + + local is_api_9_1 = true + if dt.configuration.api_version_string < "9.1.0" then + is_api_9_1 = false + end + + local datetime = os.date("*t") + local user_name = username or USER + local pictures_folder = pic_folder or PICTURES + local home_folder = home or HOME + log.msg(log.info, "home is " .. tostring(home) .. " and HOME is " .. tostring(HOME) .. " and home_folder is " .. tostring(home_folder)) + local desktop_folder = desktop or DESKTOP + + local labels = get_colorlabels(image) + log.msg(log.info, "image date time taken is " .. image.exif_datetime_taken) + local eyear, emon, eday, ehour, emin, esec, emsec + if dt.preferences.read("darktable", "lighttable/ui/milliseconds", "bool") and is_api_9_1 then + eyear, emon, eday, ehour, emin, esec, emsec = + string.match(image.exif_datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)%.(%d+)$") + else + emsec = "0" + eyear, emon, eday, ehour, emin, esec = + string.match(image.exif_datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)$") + end + local replacements = {image.film, -- ROLL.NAME + image.path, -- FILE.FOLDER + image.filename, -- FILE.NAME + dtutils_string.get_filetype(image.filename),-- FILE.EXTENSION + image.id, -- ID + image.duplicate_index, -- VERSION + "", -- VERSION.IF_MULTI + image.version_name, -- VERSION.NAME + dt.configuration.version, -- DARKTABLE.VERSION + "", -- DARKTABLE.NAME + sequence, -- SEQUENCE + image.width, -- WIDTH.SENSOR + image.height, -- HEIGHT.SENSOR + is_api_9_1 and image.p_width or "", -- WIDTH.RAW + is_api_9_1 and image.p_height or "", -- HEIGHT.RAW + is_api_9_1 and image.final_width or "", -- WIDTH.CROP + is_api_9_1 and image.final_height or "", -- HEIGHT.CROP + is_api_9_1 and image.final_width or "", -- WIDTH.EXPORT + is_api_9_1 and image.final_height or "", -- HEIGHT.EXPORT + "", -- WIDTH.MAX + "", -- HEIGHT.MAX + string.format("%4d", datetime.year), -- YEAR + string.format("%02d", datetime.month), -- MONTH + string.format("%02d", datetime.day), -- DAY + string.format("%02d", datetime.hour), -- HOUR + string.format("%02d", datetime.min), -- MINUTE + string.format("%02d", datetime.sec), -- SECOND + "", -- MSEC + eyear, -- EXIF.YEAR + emon, -- EXIF.MONTH + eday, -- EXIF.DAY + ehour, -- EXIF.HOUR + emin, -- EXIF.MINUTE + esec, -- EXIF.SECOND + emsec, -- EXIF.MSEC + "", -- EXIF.DATE.REGIONAL + "", -- EXIF.TIME.REGIONAL + string.format("%d", image.exif_iso), -- EXIF.ISO + string.format("1/%.0f", 1./image.exif_exposure), -- EXIF.EXPOSURE + image.exif_exposure_bias, -- EXIF.EXPOSURE.BIAS + string.format("%.01f", image.exif_aperture), -- EXIF.APERTURE + string.format("%.0f", image.exif_focal_length), -- EXIF.FOCAL.LENGTH + image.exif_focus_distance, -- EXIF.FOCUS.DISTANCE + image.longitude or "", -- LONGITUDE + image.latitude or "", -- LATITUDE + image.elevation or "", -- ALTITUDE + image.rating, -- STARS + "", -- RATING.ICONS + labels, -- LABELS + "", -- LABELS.ICONS + image.exif_maker, -- MAKER + image.exif_model, -- MODEL + image.title, -- TITLE + image.description, -- DESCRIPTION + image.creator, -- CREATOR + image.publisher, -- PUBLISHER + image.rights, -- RIGHTS + "", -- TAGS + "", -- CATEGORYn + "", -- SIDECAR.TXT + pictures_folder, -- FOLDER.PICTURES + home_folder, -- FOLDER.HOME + desktop_folder, -- FOLDER.DESKTOP + "", -- OPENCL.ACTIVATED + user_name, -- USERNAME + "\n", -- NL + "" -- JOBCODE + } + + for i = 1, #PLACEHOLDERS, 1 do + substitutes[PLACEHOLDERS[i]] = replacements[i] + log.msg(log.info, "setting " .. PLACEHOLDERS[i] .. " to " .. tostring(replacements[i])) + end + log.log_level(old_log_level) +end + +local function check_legacy_vars(var_name) + local var = var_name + if string.match(var, "_") then + var = string.gsub(var, "_", ".") + end + if string.match(var, "^HOME$") then var = "FOLDER.HOME" end + if string.match(var, "^PICTURES.FOLDER$") then var = "FOLDER.PICTURES" end + if string.match(var, "^DESKTOP$") then var = "FOLDER.DESKTOP" end + return var +end + +local function treat(var_string) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + local ret_val = "" + -- remove the var from the string + local var = string.match(var_string, "[%a%._]+") + var = check_legacy_vars(var) + log.msg(log.info, "var_string is " .. tostring(var_string) .. " and var is " .. tostring(var)) + ret_val = substitutes[var] + if not ret_val then + log.msg(log.error, "variable " .. var .. " is not an allowed variable, returning empty value") + log.log_level(old_log_level) + return "" + end + local args = string.gsub(var_string, var, "") + log.msg(log.info, "args is " .. tostring(args)) + if string.len(args) > 0 then + if string.match(args, '^%^%^') then + ret_val = string.upper(ret_val) + elseif string.match(args, "^%^") then + ret_val = string.gsub(ret_val, "^%a", string.upper, 1) + elseif string.match(args, "^,,") then + ret_val = string.lower(ret_val) + elseif string.match(args, "^,") then + ret_val = string.gsub(ret_val, "^%a", string.lower, 1) + elseif string.match(args, "^:%-?%d+:%-?%d+") then + local soffset, slen = string.match(args, ":(%-?%d+):(%-?%d+)") + log.msg(log.info, "soffset is " .. soffset .. " and slen is " .. slen) + if tonumber(soffset) >= 0 then + soffset = soffset + 1 + end + log.msg(log.info, "soffset is " .. soffset .. " and slen is " .. slen) + if tonumber(soffset) < 0 and tonumber(slen) < 0 then + local temp = soffset + soffset = slen + slen = temp + end + log.msg(log.info, "soffset is " .. soffset .. " and slen is " .. slen) + ret_val = string.sub(ret_val, soffset, slen) + log.msg(log.info, "ret_val is " .. ret_val) + elseif string.match(args, "^:%-?%d+") then + local soffset= string.match(args, ":(%-?%d+)") + if tonumber(soffset) >= 0 then + soffset = soffset + 1 + end + ret_val = string.sub(ret_val, soffset, -1) + elseif string.match(args, "^-%$%(.-%)") then + local replacement = string.match(args, "-%$%(([%a%._]+)%)") + replacement = check_legacy_vars(replacement) + if string.len(ret_val) == 0 then + ret_val = substitutes[replacement] + end + elseif string.match(args, "^-.+$") then + local replacement = string.match(args, "-(.+)$") + if string.len(ret_val) == 0 then + ret_val = replacement + end + elseif string.match(args, "^+.+") then + local replacement = string.match(args, "+(.+)") + if string.len(ret_val) > 0 then + ret_val = replacement + end + elseif string.match(args, "^#.+") then + local pattern = string.match(args, "#(.+)") + log.msg(log.info, "pattern to remove is " .. tostring(pattern)) + ret_val = string.gsub(ret_val, "^" .. dtutils_string.sanitize_lua(pattern), "") + elseif string.match(args, "^%%.+") then + local pattern = string.match(args, "%%(.+)") + ret_val = string.gsub(ret_val, pattern .. "$", "") + elseif string.match(args, "^//.-/.+") then + local pattern, replacement = string.match(args, "//(.-)/(.+)") + ret_val = string.gsub(ret_val, pattern, replacement) + elseif string.match(args, "^/#.+/.+") then + local pattern, replacement = string.match(args, "/#(.+)/(.+)") + ret_val = string.gsub(ret_val, "^" .. pattern, replacement, 1) + elseif string.match(args, "^/%%.-/.+") then + local pattern, replacement = string.match(args, "/%%(.-)/(.+)") + ret_val = string.gsub(ret_val, pattern .. "$", replacement) + elseif string.match(args, "^/.-/.+") then + log.msg(log.info, "took replacement branch") + local pattern, replacement = string.match(args, "/(.-)/(.+)") + ret_val = string.gsub(ret_val, pattern, replacement, 1) + end + end + log.log_level(old_log_level) + return ret_val +end + +dtutils_string.libdoc.functions["substitute_list"] = { + Name = [[substitute_list]], + Synopsis = [[Replace variables in a string with their computed values]], + Usage = [[local ds = require "lib/dtutils.string" + local result = ds.substitute_list(str) + str - string - the string containing the variables to be substituted for]], + Description = [[substitute_list replaces the variables in the supplied string with + values computed in build_substitution_list().]], + Return_Value = [[result - string - the input string with values substituted for the variables]], + Limitations = [[]], + Example = [[]], + See_Also = [[]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +function dtutils_string.substitute_list(str) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + -- replace the substitution variables in a string + for match in string.gmatch(str, "%$%(.-%)?%)") do + local var = string.match(match, "%$%((.-%)?)%)") + local treated_var = treat(var) + log.msg(log.info, "var is " .. var .. " and treated var is " .. tostring(treated_var)) + str = string.gsub(str, "%$%(".. dtutils_string.sanitize_lua(var) .."%)", tostring(treated_var)) + log.msg(log.info, "str after replacement is " .. str) + end + log.log_level(old_log_level) + return str +end + +dtutils_string.libdoc.functions["clear_substitute_list"] = { + Name = [[clear_substitute_list]], + Synopsis = [[Clear the computed list of variable substitution values]], + Usage = [[local ds = require "lib/dtutils.string" + ds.clear_substitute_list()]], + Description = [[clear_substitute_list resets the list of variable replacement values]], + Return_Value = [[]], + Limitations = [[]], + Example = [[]], + See_Also = [[]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +function dtutils_string.clear_substitute_list() + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + for i = 1, #PLACEHOLDERS, 1 do + substitutes[PLACEHOLDERS[i]] = nil + end + log.log_level(old_log_level) +end + +dtutils_string.libdoc.functions["substitute"] = { + Name = [[substitute]], + Synopsis = [[Check if a string has been sanitized]], + Usage = [[local ds = require "lib/dtutils.string" + ds.substitute(image, sequence, [username], [pic_folder], [home], [desktop]) + image - dt_lua_image_t - the image being processed + sequence - integer - the sequence number of the image + [username] - string - optional - user name. Will be determined if not supplied + [pic_folder] - string - optional - pictures folder name. Will be determined if not supplied + [home] - string - optional - home directory. Will be determined if not supplied + [desktop] - string - optional - desktop directory. Will be determined if not supplied]], + Description = [[substitute initializes the substitution list by calling clear_substitute_list(), + then builds the substitutions by calling build_substitution_list() and finally does the + substitution by calling substitute_list(), then returns the result string.]], + Return_Value = [[result - string - the input string with values substituted for the variables]], + Limitations = [[]], + Example = [[]], + See_Also = [[]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +function dtutils_string.substitute(image, sequence, variable_string, username, pic_folder, home, desktop) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + dtutils_string.clear_substitute_list() + dtutils_string.build_substition_list(image, sequence, username, pic_folder, home, desktop) + local str = dtutils_string.substitute_list(variable_string) + log.log_level(old_log_level) + return str +end + return dtutils_string