From b49352e1e670cbe497bd6985bff8d908d708de58 Mon Sep 17 00:00:00 2001 From: Alex S Date: Sun, 7 Sep 2025 19:13:40 +0200 Subject: [PATCH 1/3] HDRMerge: use action_images to allow merging a selected group --- contrib/HDRMerge.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/HDRMerge.lua b/contrib/HDRMerge.lua index 9d7a951d..408704a5 100644 --- a/contrib/HDRMerge.lua +++ b/contrib/HDRMerge.lua @@ -219,7 +219,7 @@ local function main() dt.print(_('HDRMerge install issue, please ensure the binary path is correct')) return end - images = dt.gui.selection() --get selected images + images = dt.gui.action_images --get selected images if #images < 2 then --ensure enough images selected dt.print(_('not enough images selected, select at least 2 images to merge')) return @@ -483,4 +483,4 @@ script_data.restart = restart script_data.destroy_method = "hide" script_data.show = restart -return script_data \ No newline at end of file +return script_data From 93fe3b62c035be7a525b22c5a7f52e2382daa06c Mon Sep 17 00:00:00 2001 From: Alex S Date: Sun, 7 Sep 2025 19:25:59 +0200 Subject: [PATCH 2/3] HDRMerge: add "groups" mode For workflow together with e.g AutoGrouper, gives the ability to select multiple bracketed groups and runs HDRMerge on each of them, adding the result back to the group. --- contrib/HDRMerge.lua | 188 ++++++++++++++++++++++++++++++------------- 1 file changed, 134 insertions(+), 54 deletions(-) diff --git a/contrib/HDRMerge.lua b/contrib/HDRMerge.lua index 408704a5..252d6eba 100644 --- a/contrib/HDRMerge.lua +++ b/contrib/HDRMerge.lua @@ -34,10 +34,13 @@ Additional tags or style can be applied on auto import as well, if you desire. Base Options: Select your desired BPS (bits per sample and Embedded Preview Size. -Batch Options: +Batch Mode Options: Select if you want to run in batch mode or not Select the gap, in seconds, between images for auto grouping in batch mode +Group Mode Options: +The HDR image will be placed back in its group. Select if you want it to become the group leader. + See HDRMerge manual for further detail: http://jcelaya.github.io/hdrmerge/documentation/2014/07/11/user-manual.html Auto-import Options: @@ -208,61 +211,55 @@ end local function UpdateActivePreference() --sliders & entry boxes do not have a click/changed callback, so their values must be saved to the active preference temp = GUI.HDR.gap.value dt.preferences.write(mod, 'active_gap', 'integer', temp) + temp = GUI.HDR.make_group_leader.value + dt.preferences.write(mod, 'active_make_group_leader', 'bool', temp) temp = GUI.Target.add_tags.text dt.preferences.write(mod, 'active_add_tags', 'string', temp) end -local function main() - PreCall({HDRM}) --check if furst run then check if install OK - if HDRM.install_error then - dt.print_error('HDRMerge install issue') - dt.print(_('HDRMerge install issue, please ensure the binary path is correct')) - return - end - images = dt.gui.action_images --get selected images - if #images < 2 then --ensure enough images selected - dt.print(_('not enough images selected, select at least 2 images to merge')) - return - end - - UpdateActivePreference() --save current gui elements to active preference so those values will be pre-loaded at next startup - - --create image string and output path - HDRM.images_string = '' +local function DoBatch(prog_table, images, output_arg) + local prog = {} + -- Copy program arguments + for k, v in pairs(prog_table) do prog[k] = v end local out_path = '' local smallest_id = math.huge local smallest_name = '' local largest_id = 0 local source_raw = {} - for _,image in pairs(images) do --loop to concat the images string, also track the image indexes for use in creating the final image name (eg; IMG_1034-1037.dng) - local curr_image = image.path..os_path_seperator..image.filename - HDRM.images_string = HDRM.images_string..df.sanitize_filename(curr_image)..' ' - out_path = image.path - _unused, source_name, source_id = GetFileName(image.filename) - source_id = tonumber(source_id) or 0 - if source_id < smallest_id then - smallest_id = source_id - smallest_name = source_name - source_raw = image + local image_files = {} + for _,image in ipairs(images) do --loop to concat the images string, also track the image indexes for use in creating the final image name (eg; IMG_1034-1037.dng) + if not image.is_hdr then + num_images = num_images + 1 + local curr_image = image.path..os_path_seperator..image.filename + table.insert(image_files, df.sanitize_filename(curr_image)) + _unused, source_name, source_id = GetFileName(image.filename) + source_id = tonumber(source_id) or 0 + if source_id < smallest_id then + out_path = image.path + smallest_id = source_id + smallest_name = source_name + source_raw = image + end + if source_id > largest_id then largest_id = source_id end end - if source_id > largest_id then largest_id = source_id end end - out_path = out_path..os_path_seperator..smallest_name..'-'..largest_id..'.dng' - out_path = df.create_unique_filename(out_path) - - --create argument string - HDRM.arg_string = HDRM.args.bps.text..GUI.HDR.bps.value..' '..HDRM.args.size.text..GUI.HDR.size.value..' ' - if GUI.HDR.batch.value then - HDRM.arg_string = HDRM.arg_string..HDRM.args.batch.text..HDRM.args.gap.text..math.floor(GUI.HDR.gap.value)..' -a' - else - HDRM.arg_string = HDRM.arg_string..'-o '..df.sanitize_filename(out_path) + if #image_files < 2 then + dt.print(_("not enough images in group, skipping")) + return + end + prog.images_string = table.concat(image_files, ' ') + if output_arg then + out_path = out_path..os_path_seperator..smallest_name..'-'..largest_id..'.dng' + out_path = df.create_unique_filename(out_path) + prog.arg_string = prog.arg_string..'-o '..df.sanitize_filename(out_path) end -- create run command and execute - local run_cmd = BuildExecuteCmd(HDRM) + local run_cmd = BuildExecuteCmd(prog) + dt.print_log('running HDRMerge: '..run_cmd) resp = dsys.external_command(run_cmd) - if resp == 0 and not GUI.HDR.batch.value then + if resp == 0 and output_arg then local imported = dt.database.import(out_path) -- import the new file if GUI.Target.style.selected > 1 then -- apply selected style local set_style = styles[GUI.Target.style.selected - 1] @@ -283,11 +280,65 @@ local function main() end end dt.print(_('HDRMerge completed successfully')) - else + return imported + elseif resp == 0 then + dt.print(_('HDRMerge completed successfully')) + return nil -- TODO: can detect created files? + elseif resp ~= 0 then dt.print_error('HDRMerge failed') dt.print(_('HDRMerge failed')) end +end +local function main() + PreCall({HDRM}) --check if furst run then check if install OK + if HDRM.install_error then + dt.print_error('HDRMerge install issue') + dt.print(_('HDRMerge install issue, please ensure the binary path is correct')) + return + end + images = dt.gui.action_images --get selected images + if #images < 2 then --ensure enough images selected + dt.print(_('not enough images selected, select at least 2 images to merge')) + return + end + + UpdateActivePreference() --save current gui elements to active preference so those values will be pre-loaded at next startup + + --create argument string + HDRM.arg_string = HDRM.args.bps.text..GUI.HDR.bps.value..' '..HDRM.args.size.text..GUI.HDR.size.value..' ' + if GUI.HDR.mode.selected == 2 then + end + + -- do mode operation + if GUI.HDR.mode.selected == 1 then + doBatch(HDRM, images, true) + elseif GUI.HDR.mode.selected == 2 then + HDRM.arg_string = HDRM.arg_string..HDRM.args.batch.text..HDRM.args.gap.text..math.floor(GUI.HDR.gap.value)..' -a' + dBatch(HDRM, images, false) + elseif GUI.HDR.mode.selected == 3 then + local by_group = {} + for _, im in ipairs(images) do + local leader = im.group_leader + if not by_group[leader] then by_group[leader] = {} end + table.insert(by_group[leader], im) + end + for leader, group in pairs(by_group) do + local imported = DoBatch(HDRM, group, true) + if imported then + imported:group_with(leader) + + if GUI.HDR.make_group_leader.value then + imported:make_group_leader() + end + end + if dt.gui.libs.global_toolbox.grouping then + -- Toggle off & on to update the view + dt.gui.libs.global_toolbox.grouping = false + dt.gui.libs.global_toolbox.grouping = true + end + end + end end local function install_module() @@ -353,21 +404,12 @@ GUI.HDR.size = dt.new_widget('combobox'){ dt.preferences.write(mod, 'active_size_ind', 'integer', self.selected) end } -GUI.HDR.batch = dt.new_widget('check_button'){ - label = _('batch mode'), - value = dt.preferences.read(mod, 'active_batch', 'bool'), - tooltip = _('enable batch mode operation \nNOTE: resultant files will NOT be auto-imported'), - clicked_callback = function(self) - dt.preferences.write(mod, 'active_batch', 'bool', self.value) - GUI.HDR.gap.sensitive = self.value - end, - reset_callback = function(self) self.value = false end -} + temp = dt.preferences.read(mod, 'active_gap', 'integer') if not InRange(temp, 1, 3600) then temp = 3 end GUI.HDR.gap = dt.new_widget('slider'){ label = _('batch gap [sec.]'), - tooltip = _('gap, in seconds, between batch mode groups'), + tooltip = _('gap, in seconds, between batch mode images within group'), soft_min = 1, soft_max = 30, hard_min = 1, @@ -380,6 +422,44 @@ GUI.HDR.gap = dt.new_widget('slider'){ self.value = 3 end } + +GUI.HDR.make_group_leader = dt.new_widget('check_button'){ + label = _('make group leader'), + tooltip = _('make new image the group leader'), + value = dt.preferences.read(mod, 'active_make_group_leader', 'bool') +} +temp = dt.preferences.read(mod, 'active_mode', 'integer') +if not InRange(temp, 1, 3) then + if dt.preferences.read(mod, 'active_batch', 'bool') then + temp = 2 + else + temp = 1 + end +end +GUI.HDR.opts = dt.new_widget('stack'){ + dt.new_widget('box'), + GUI.HDR.gap, + GUI.HDR.make_group_leader, + active = temp +} +GUI.HDR.mode = dt.new_widget('combobox'){ + label = _('mode'), + tooltip = _([[Operating mode: + all: merge all images into a single image + batch: group images into sets by comparing creation time. Images will NOT be auto-imported + groups: merge images according to image grouping]]), + value = temp, + changed_callback = function(self) + dt.preferences.write(mod, 'active_mode', 'integer', self.selected) + GUI.HDR.opts.active = self.selected + end, + reset_callback = function(self) self.selected = 1 end, + _('all'), + _('batch'), + _('groups') +} + + local lbl_import = dt.new_widget('section_label'){ label = _('import options') } @@ -439,8 +519,8 @@ GUI.options = dt.new_widget('box'){ lbl_hdr, GUI.HDR.bps, GUI.HDR.size, - GUI.HDR.batch, - GUI.HDR.gap, + GUI.HDR.mode, + GUI.HDR.opts, lbl_import, GUI.Target.style, GUI.Target.copy_tags, From dd98714739d41921c505a010ec522cd4f0a83042 Mon Sep 17 00:00:00 2001 From: Alex S Date: Sun, 7 Sep 2025 19:39:24 +0200 Subject: [PATCH 3/3] HDRMerge: allow non-word characters in filenames Previous filename generation led to e.g timestamps like 20:00:03 getting cut apart. --- contrib/HDRMerge.lua | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/contrib/HDRMerge.lua b/contrib/HDRMerge.lua index 252d6eba..ea6a7214 100644 --- a/contrib/HDRMerge.lua +++ b/contrib/HDRMerge.lua @@ -138,23 +138,20 @@ local function InRange(test, low, high) --tests if test value is within range of end end -local function GetFileName(full_path) --Parses a full path (path/filename_identifier.extension) into individual parts ---[[Input: Folder1/Folder2/Folder3/Img_0001.CR2 +local function FileNameParts(filename_ext) --Parses a filename (filename_identifier.extension) into individual parts +--[[Input: Img_0001.CR2 Returns: - path: Folder1/Folder2/Folder3/ filename: Img_0001 identifier: 0001 - extension: .CR2 EX: - path_1, file_1, id_1, ext_1 = GetFileName(full_path_1) + file_1, id_1 = FileNameParts(im_filename) ]] - local path = string.match(full_path, '.*[\\/]') - local filename = string.gsub(string.match(full_path, '[%w-_]*%.') , '%.' , '' ) - local identifier = string.match(filename, '%d*$') - local extension = string.match(full_path, '%.%w*') - return path, filename, identifier, extension + local ext_idx = string.find(filename_ext, '%.[^%.]*$') + local filename = string.sub(filename_ext, 1, ext_idx and ext_idx - 1 or nil) + local identifier = string.match(filename, '%d*$') or '' + return filename, identifier end local function CleanSpaces(text) --removes spaces from the front and back of passed in text @@ -222,25 +219,28 @@ local function DoBatch(prog_table, images, output_arg) -- Copy program arguments for k, v in pairs(prog_table) do prog[k] = v end local out_path = '' - local smallest_id = math.huge + local smallest_id_n = math.huge local smallest_name = '' - local largest_id = 0 + local largest_id_n = -1 + local largest_id = '' local source_raw = {} local image_files = {} for _,image in ipairs(images) do --loop to concat the images string, also track the image indexes for use in creating the final image name (eg; IMG_1034-1037.dng) if not image.is_hdr then - num_images = num_images + 1 local curr_image = image.path..os_path_seperator..image.filename table.insert(image_files, df.sanitize_filename(curr_image)) - _unused, source_name, source_id = GetFileName(image.filename) - source_id = tonumber(source_id) or 0 - if source_id < smallest_id then + source_name, source_id = FileNameParts(image.filename) + local source_id_n = tonumber(source_id) or 0 + if source_id_n < smallest_id_n then out_path = image.path - smallest_id = source_id + smallest_id_n = source_id_n smallest_name = source_name source_raw = image end - if source_id > largest_id then largest_id = source_id end + if source_id_n > largest_id_n then + largest_id = source_id + largest_id_n = source_id_n + end end end if #image_files < 2 then