From 9bfda93b510d16fb2b0d989626312f4449a4e367 Mon Sep 17 00:00:00 2001 From: Bill Ferguson Date: Wed, 13 Nov 2024 23:09:14 -0500 Subject: [PATCH 01/41] official/apply_camera_style - make the translatable strings contrib/hif_group_leader translation safe contrib/jpg_group_leader --- contrib/hif_group_leader.lua | 4 ++-- contrib/jpg_group_leader.lua | 4 ++-- official/apply_camera_style.lua | 6 ++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/contrib/hif_group_leader.lua b/contrib/hif_group_leader.lua index f39d3ff..412eea0 100644 --- a/contrib/hif_group_leader.lua +++ b/contrib/hif_group_leader.lua @@ -186,7 +186,7 @@ dt.register_event(MODULE .. "_collect", "shortcut", local images = dt.collection make_existing_hif_group_leader(images) end, - string.format(_("make hif group leader for %s", _("collection"))) + _("make hif group leader for collection") ) dt.register_event(MODULE .. "_select", "shortcut", @@ -194,7 +194,7 @@ dt.register_event(MODULE .. "_select", "shortcut", local images = dt.gui.selection() make_existing_hif_group_leader(images) end, - string.format(_("make hif group leader for %s", _("selection"))) + _("make hif group leader for selection") ) return script_data \ No newline at end of file diff --git a/contrib/jpg_group_leader.lua b/contrib/jpg_group_leader.lua index 8391a67..733071e 100644 --- a/contrib/jpg_group_leader.lua +++ b/contrib/jpg_group_leader.lua @@ -186,7 +186,7 @@ dt.register_event(MODULE .. "_collect", "shortcut", local images = dt.collection make_existing_jpg_group_leader(images) end, - string.format(_("make jpg group leader for %s", _("collection"))) + _("make jpg group leader for collection") ) dt.register_event(MODULE .. "_select", "shortcut", @@ -194,7 +194,7 @@ dt.register_event(MODULE .. "_select", "shortcut", local images = dt.gui.selection() make_existing_jpg_group_leader(images) end, - string.format(_("make jpg group leader for %s", _("selection"))) + _("make jpg group leader for selection") ) return script_data \ No newline at end of file diff --git a/official/apply_camera_style.lua b/official/apply_camera_style.lua index 8c8aeae..c75974b 100644 --- a/official/apply_camera_style.lua +++ b/official/apply_camera_style.lua @@ -463,18 +463,16 @@ 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")) + end, _("apply darktable camera styles to collection") ) dt.register_event(MODULE, "shortcut", function(event, shortcut) apply_camera_style(false) - end, string.format(shortcut_string, _("selection")) + end, _("apply darktable camera styles to selection") ) dt.register_event(MODULE, "post-import-image", From b5c93e55d16e52e3d3bdafe603dac85cdb3ac895 Mon Sep 17 00:00:00 2001 From: Bill Ferguson Date: Wed, 13 Nov 2024 19:05:16 -0500 Subject: [PATCH 02/41] lib/dtutils - added a gen_uuid() function to generate a UUID string for use as a unique identifier. --- lib/dtutils.lua | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/dtutils.lua b/lib/dtutils.lua index c8331cb..6721eac 100644 --- a/lib/dtutils.lua +++ b/lib/dtutils.lua @@ -374,7 +374,7 @@ dtutils.libdoc.functions["deprecated"] = { du.deprecated(script_name, removal_string) script_name - name of the script being deprecated - removal_strubg - a string explaining when the script will be removed]], + removal_string - a string explaining when the script will be removed]], Description = [[deprecated prints an error message saying the script is deprecated and when it will be removed]], Return_Value = [[]], Limitations = [[]], @@ -391,5 +391,40 @@ function dtutils.deprecated(script_name, removal_string) dt.print_error("WARNING: " .. script_name .. " is deprecated and will be removed in " .. removal_string) end +dtutils.libdoc.functions["gen_uuid"] = { + Name = [[gen_uuid]], + Synopsis = [[generate a UUID string]], + Usage = [[local du = require "lib/dtutils" + + uuid = du.gen_uuid(case) + case - "upper" or "lower" to specify the case of the UUID string]], + Description = [[gen_uuid prints an error message saying the script is gen_uuid and when it will be removed]], + Return_Value = [[uuid - string - a hexidecimal string representing the UUID in the requested case]], + Limitations = [[]], + Example = [[]], + See_Also = [[]], + Reference = [[https://gist.github.com/jrus/3197011]], + License = [[]], + Copyright = [[]], +} + +function dtutils.gen_uuid(case) + local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + + -- seed with os.time in seconds and add an extra degree of random for multiple calls in the same second + math.randomseed(os.time(), math.random(0, 65536)) + + local uuid = string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) + return string.format('%x', v) + end + ) + + if case and case == "upper" then + uuid = string.upper(uuid) + end + + return uuid +end return dtutils From ed9f2bb0d055754c12058098af4d53336453f7cc Mon Sep 17 00:00:00 2001 From: Bill Ferguson Date: Sun, 1 Dec 2024 12:27:16 -0500 Subject: [PATCH 03/41] official/apply_camera_sytle - ensure style is only applied to raw images. Fixed comment. --- official/apply_camera_style.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/official/apply_camera_style.lua b/official/apply_camera_style.lua index c75974b..aac49d1 100644 --- a/official/apply_camera_style.lua +++ b/official/apply_camera_style.lua @@ -64,7 +64,7 @@ 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 +du.check_min_api_version("9.4.0", MODULE) -- camera styles added to darktable 5.0 -- - - - - - - - - - - - - - - - - - - - - - - - - - @@ -477,7 +477,9 @@ dt.register_event(MODULE, "shortcut", dt.register_event(MODULE, "post-import-image", function(event, image) - table.insert(acs.imported_images, image) + if image.is_raw then + table.insert(acs.imported_images, image) + end end ) From 9bceb79a150c41f4aaa3e3546722ed03fc4ca57d Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Mon, 2 Dec 2024 09:01:48 +0100 Subject: [PATCH 04/41] Fixed $(FILE.NAME) variable substitution According to comments and https://docs.darktable.org/usermanual/4.6/en/special-topics/variables/ it should be a basename. --- lib/dtutils/string.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dtutils/string.lua b/lib/dtutils/string.lua index c8a1ce9..a710201 100644 --- a/lib/dtutils/string.lua +++ b/lib/dtutils/string.lua @@ -748,7 +748,7 @@ function dtutils_string.build_substitute_list(image, sequence, variable_string, local replacements = {image.film.path, -- ROLL.NAME image.path, -- FILE.FOLDER - image.filename, -- FILE.NAME + dtutils_string.get_basename(image.filename),-- FILE.NAME dtutils_string.get_filetype(image.filename),-- FILE.EXTENSION image.id, -- ID image.duplicate_index, -- VERSION From d27df0c0ed31a26bd9b68eca5831e49751fce12a Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Mon, 2 Dec 2024 09:56:45 +0100 Subject: [PATCH 05/41] Fixed $ROLL.NAME substitution Accd to https://github.com/darktable-org/darktable/blob/57d3ad4bd30372b1f7ae7368f6887d3292269b2a/src/common/variables.c#L660 it should be a basename of the file path. --- lib/dtutils/string.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dtutils/string.lua b/lib/dtutils/string.lua index a710201..2be4ef6 100644 --- a/lib/dtutils/string.lua +++ b/lib/dtutils/string.lua @@ -746,7 +746,7 @@ function dtutils_string.build_substitute_list(image, sequence, variable_string, local version_multi = #image:get_group_members() > 1 and image.version or "" - local replacements = {image.film.path, -- ROLL.NAME + local replacements = {dtutils_string.get_basename(image.film.path),-- ROLL.NAME image.path, -- FILE.FOLDER dtutils_string.get_basename(image.filename),-- FILE.NAME dtutils_string.get_filetype(image.filename),-- FILE.EXTENSION From 87ca8bde27219034a6e250d19cea14f0be6f1ca6 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Mon, 2 Dec 2024 10:11:23 +0100 Subject: [PATCH 06/41] Fixed $(VERSION.IF_MULTI) Variable substitution actually errored out when duplicates were present. --- lib/dtutils/string.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dtutils/string.lua b/lib/dtutils/string.lua index 2be4ef6..7648f6f 100644 --- a/lib/dtutils/string.lua +++ b/lib/dtutils/string.lua @@ -744,7 +744,7 @@ function dtutils_string.build_substitute_list(image, sequence, variable_string, string.match(image.exif_datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)$") end - local version_multi = #image:get_group_members() > 1 and image.version or "" + local version_multi = #image:get_group_members() > 1 and image.duplicate_index or "" local replacements = {dtutils_string.get_basename(image.film.path),-- ROLL.NAME image.path, -- FILE.FOLDER From 3b520d62918dc21129bd0672ea0b90b1edbfd9aa Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Fri, 23 Aug 2024 17:53:03 +0200 Subject: [PATCH 07/41] Added UltraHDR export plugin to generate UltraHDR JPEG images. --- contrib/ultrahdr.lua | 270 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 contrib/ultrahdr.lua diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua new file mode 100644 index 0000000..f3b4d19 --- /dev/null +++ b/contrib/ultrahdr.lua @@ -0,0 +1,270 @@ +--[[ + + UltraHDR storage for darktable + + copyright (c) 2024 Krzysztof Kotowicz + + 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 . + +]] +--[[ + +ULTRAHDR +Add a new storage option to generate UltraHDR JPG images. + +https://developer.android.com/media/platform/hdr-image-format + +Of all exported files, the storage detects pairs of files generated from the same source image, +assuming the first one encountered is the base SDR image, and the second one is the gainmap +(alternatively, you can tag the gainmaps with a "gainmap" tag). + +The images are merged using libultrahdr example application (ultrahdr_app). + +ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT +* ultrahdr_app (from https://github.com/google/libultrahdr example dir) +* exiftool + +USAGE +* require this file from your main luarc config file +* set exiftool and libultrahdr_app tool paths + +This plugin will add a new storage option. + +]] +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local log = require "lib/dtutils.log" +local dtsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext + +local namespace = 'module_ultrahdr' + +-- works with darktable API version from 5.0.0 on +du.check_min_api_version("7.0.0", "ultrahdr") + +dt.gettext.bindtextdomain("ultrahdr", dt.configuration.config_dir .. "/lua/locale/") + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = "ultrahdr", + purpose = _("generate UltraHDR images"), + author = "Krzysztof Kotowicz" +} + +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, otherwise leave as nil +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 + +local function image_path(image) + return image.path .. "/" .. image.filename +end + +local function merge_ultrahdr(base, gainmap, ultrahdr_app, exiftool, metadata, output) + local base_tmp = df.chop_filetype(base) .. ".tmp" + local hdr = df.chop_filetype(base) .. "_hdr." .. df.get_filetype(base) + dtsys.external_command(exiftool .. " -all= " .. df.sanitize_filename(base) .. " -o " .. + df.sanitize_filename(base_tmp)) + dtsys.external_command(exiftool .. " -all= " .. df.sanitize_filename(gainmap) .. " -overwrite_original") + dtsys.external_command(ultrahdr_app .. " -m 0 -i " .. df.sanitize_filename(base_tmp) .. " -g " .. + df.sanitize_filename(gainmap) .. " -f " .. df.sanitize_filename(metadata) .. " -z " .. + df.sanitize_filename(hdr)) + dtsys.external_command(exiftool .. " -tagsfromfile " .. df.sanitize_filename(base) .. " -all>all " .. + df.sanitize_filename(hdr) .. " -overwrite_original") + df.file_move(hdr, df.create_unique_filename(output .. "/" .. df.get_filename(hdr))) + os.remove(base) + os.remove(base_tmp) + os.remove(gainmap) +end + +local function assert_settings_correct() + local ultrahdr_app = df.check_if_bin_exists("ultrahdr_app") + log.msg(log.debug, "ultrahdr_app set to ", ultrahdr_app) + local exiftool = df.check_if_bin_exists("exiftool") + log.msg(log.debug, "exiftool set to ", exiftool) + local metadata = dt.preferences.read("ultrahdr", "metadata path", "string") + log.msg(log.debug, "metadata set to ", metadata) + local output = dt.preferences.read("ultrahdr", "output dir", "string") + log.msg(log.debug, "output dir set to ", output) + + if not ultrahdr_app then + dt.print(_("ultrahdr_app is not found, did you set the path?")) + log.msg(log.error, "ultrahdr_app executable not found. Check if the executable is installed.") + log.msg(log.error, "If the executable is installed, check that the path is set correctly.") + return + end + + if not exiftool then + dt.print(_("exiftool is not found, did you set the path?")) + log.msg(log.error, "exiftool executable not found. Check if the executable is installed.") + log.msg(log.error, "If the executable is installed, check that the path is set correctly.") + return + end + + if not df.check_if_file_exists(metadata) then + dt.print(_("metadata file not found, did you set the path?")) + log.msg(log.error, "metadata file not found.") + return + end + + return ultrahdr_app, exiftool, metadata, output +end + +local function create_hdr(storage, image_table, extra_data) -- finalize + local saved_log_level = log.log_level() + log.log_level(log.info) + + local ultrahdr_app, exiftool, metadata, output = assert_settings_correct() + if not ultrahdr_app or not exiftool then + return + end + local merged = 0 + for ignore, v in pairs(extra_data) do + if not v then + goto continue + end + local msg = string.format(_("Merging %s and %s"), df.get_filename(image_table[v["base"]]), + df.get_filename(image_table[v["gainmap"]])) + log.msg(log.info, msg) + dt.print(msg) + merge_ultrahdr(image_table[v["base"]], image_table[v["gainmap"]], ultrahdr_app, exiftool, metadata, output) + merged = merged + 1 + ::continue:: + end + for ignore, v in pairs(image_table) do + if df.check_if_file_exists(v) then os.remove(v) end + end + dt.print(string.format(_("Created %d UltraHDR image(s) in %s"), merged, output)) + log.log_level(saved_log_level) +end + +local function destroy() + dt.destroy_storage(namespace) +end + +local function is_supported(storage, format) + if format.extension == "jpg" then + return true + end + return false +end + +local function initialize(storage, format, images, high_quality, extra_data) + local saved_log_level = log.log_level() + log.log_level(log.info) + local tags = nil + -- Group images into base, gainmap pairs based on their original filename. + -- Assume that the first encountered image from each filename is a base one, unless it has a "gainmap" tag. + for k, v in pairs(images) do + local has_gainmap_tag = false + tags = dt.tags.get_tags(v) + for ignore, tag in pairs(tags) do + if tag.name == "gainmap" then + has_gainmap_tag = true + end + end + local key = image_path(v) + if extra_data[key] == nil then + extra_data[key] = {} + end + if extra_data[key]["base"] or has_gainmap_tag then + extra_data[key]["gainmap"] = v + else + extra_data[key]["base"] = v + end + end + -- remove incomplete entries + for k, v in pairs(extra_data) do + if not v["base"] or not v["gainmap"] then + extra_data[k] = nil + end + end + log.log_level(saved_log_level) + return nil +end + +local function metadata_file_widget() + local box_widgets = {} + table.insert(box_widgets, dt.new_widget("label") { + label = "libultrahdr metadata file" + }) + local path = dt.preferences.read("ultrahdr", "metadata path", "string") + if not path then + path = "" + end + table.insert(box_widgets, dt.new_widget("file_chooser_button") { + title = "select libultrahdr metadata path", + value = path, + is_directory = false, + changed_callback = function(self) + if df.check_if_file_exists(self.value) then + dt.preferences.write("ultrahdr", "metadata path", "string", self.value) + end + end + }) + + local box = dt.new_widget("box") { + orientation = "vertical", + table.unpack(box_widgets) + } + return box +end + +local function output_directory_widget() + local box_widgets = {} + table.insert(box_widgets, dt.new_widget("label") { + label = "output directory" + }) + local path = dt.preferences.read("ultrahdr", "output dir", "string") + if not path then + path = "" + end + table.insert(box_widgets, dt.new_widget("file_chooser_button") { + title = "select libultrahdr metadata path", + value = path, + is_directory = true, + changed_callback = function(self) + dt.preferences.write("ultrahdr", "output dir", "string", self.value) + end + }) + + local box = dt.new_widget("box") { + orientation = "vertical", + table.unpack(box_widgets) + } + return box +end + +local ultrahdr_widget = dt.new_widget("box") { + orientation = "vertical", + metadata_file_widget(), + output_directory_widget(), + df.executable_path_widget({"ultrahdr_app", "exiftool"}) +} + +-- Register + +dt.register_storage(namespace, _("UltraHDR JPEG"), nil, create_hdr, is_supported, initialize, ultrahdr_widget) + +script_data.destroy = destroy + +return script_data From b963eaa38ccabf69dc47c9b6de4fb973c115cd7f Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Mon, 26 Aug 2024 16:28:12 +0200 Subject: [PATCH 08/41] Added encoding variant as an explicit UI setting (though only API-4 is supported now). --- contrib/ultrahdr.lua | 102 +++++++++++++++++++++++++++++++------------ 1 file changed, 74 insertions(+), 28 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index f3b4d19..381b013 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -79,7 +79,23 @@ local function image_path(image) return image.path .. "/" .. image.filename end -local function merge_ultrahdr(base, gainmap, ultrahdr_app, exiftool, metadata, output) +local ENCODING_VARIANT_API_4 = 1 + +local function get_encoding_variant() + local encoding_variant = dt.preferences.read("ultrahdr", "encoding variant", "integer") + if not encoding_variant then + encoding_variant = ENCODING_VARIANT_API_4 + end + return encoding_variant +end + +local function merge_ultrahdr_api4(base, gainmap, ultrahdr_app, exiftool, output) + local metadata = dt.preferences.read("ultrahdr", "metadata path", "string") + if not df.check_if_file_exists(metadata) then + dt.print(_("metadata file not found, did you set the correct path?")) + log.msg(log.error, "metadata file not found.") + return + end local base_tmp = df.chop_filetype(base) .. ".tmp" local hdr = df.chop_filetype(base) .. "_hdr." .. df.get_filetype(base) dtsys.external_command(exiftool .. " -all= " .. df.sanitize_filename(base) .. " -o " .. @@ -101,8 +117,6 @@ local function assert_settings_correct() log.msg(log.debug, "ultrahdr_app set to ", ultrahdr_app) local exiftool = df.check_if_bin_exists("exiftool") log.msg(log.debug, "exiftool set to ", exiftool) - local metadata = dt.preferences.read("ultrahdr", "metadata path", "string") - log.msg(log.debug, "metadata set to ", metadata) local output = dt.preferences.read("ultrahdr", "output dir", "string") log.msg(log.debug, "output dir set to ", output) @@ -120,40 +134,42 @@ local function assert_settings_correct() return end - if not df.check_if_file_exists(metadata) then - dt.print(_("metadata file not found, did you set the path?")) - log.msg(log.error, "metadata file not found.") - return - end - - return ultrahdr_app, exiftool, metadata, output + return ultrahdr_app, exiftool, output end local function create_hdr(storage, image_table, extra_data) -- finalize local saved_log_level = log.log_level() log.log_level(log.info) - local ultrahdr_app, exiftool, metadata, output = assert_settings_correct() + local ultrahdr_app, exiftool, output = assert_settings_correct() if not ultrahdr_app or not exiftool then return end - local merged = 0 - for ignore, v in pairs(extra_data) do - if not v then - goto continue + local encoding_variant = get_encoding_variant() + log.msg(log.info, string.format("using encoding variant %d", encoding_variant)) + if encoding_variant == ENCODING_VARIANT_API_4 then + local merged = 0 + for ignore, v in pairs(extra_data) do + if not v then + goto continue + end + local msg = string.format(_("Merging %s and %s"), df.get_filename(image_table[v["base"]]), + df.get_filename(image_table[v["gainmap"]])) + log.msg(log.info, msg) + dt.print(msg) + merge_ultrahdr_api4(image_table[v["base"]], image_table[v["gainmap"]], ultrahdr_app, exiftool, output) + merged = merged + 1 + ::continue:: end - local msg = string.format(_("Merging %s and %s"), df.get_filename(image_table[v["base"]]), - df.get_filename(image_table[v["gainmap"]])) - log.msg(log.info, msg) + for ignore, v in pairs(image_table) do + if df.check_if_file_exists(v) then os.remove(v) end + end + dt.print(string.format(_("Created %d UltraHDR image(s) in %s"), merged, output)) + else + local msg = string.format(_("Unknown encoding variant: %d"), encoding_variant) + dt.print_error(msg) dt.print(msg) - merge_ultrahdr(image_table[v["base"]], image_table[v["gainmap"]], ultrahdr_app, exiftool, metadata, output) - merged = merged + 1 - ::continue:: end - for ignore, v in pairs(image_table) do - if df.check_if_file_exists(v) then os.remove(v) end - end - dt.print(string.format(_("Created %d UltraHDR image(s) in %s"), merged, output)) log.log_level(saved_log_level) end @@ -161,9 +177,14 @@ local function destroy() dt.destroy_storage(namespace) end + + local function is_supported(storage, format) - if format.extension == "jpg" then - return true + local encoding_variant = get_encoding_variant() + if encoding_variant == ENCODING_VARIANT_API_4 then + -- API-4 expects compressed base and gainmap + -- https://github.com/google/libultrahdr/tree/main?tab=readme-ov-file#encoding-api-outline + return format.extension == "jpg" end return false end @@ -254,10 +275,35 @@ local function output_directory_widget() return box end +local function encoding_variant_widget() + local metadata_widget = metadata_file_widget() + local encoding_variant = get_encoding_variant() + local combobox = dt.new_widget("combobox"){ + label = _("Generate HDR from"), + selected = encoding_variant, + changed_callback=function(self) + dt.preferences.write("ultrahdr", "encoding variant", "integer", self.selected) + if self.selected == ENCODING_VARIANT_API_4 then + metadata_widget.visible = true + else + metadata_widget.visible = false + end + end, + _("SDR + gainmap (API-4)") -- ENCODING_VARIANT_API_4, + } + + combobox.changed_callback(combobox) + return dt.new_widget("box") { + orientation = "vertical", + combobox, + metadata_widget + } +end + local ultrahdr_widget = dt.new_widget("box") { orientation = "vertical", - metadata_file_widget(), output_directory_widget(), + encoding_variant_widget(), df.executable_path_widget({"ultrahdr_app", "exiftool"}) } From 1986f4e14843453274f571b4f3a534beaf04b8d5 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Thu, 19 Sep 2024 12:49:04 +0200 Subject: [PATCH 09/41] Major overhaul of the plugin. Added SDR + HDR (JPEG-XL) support. --- contrib/ultrahdr.lua | 663 +++++++++++++++++++++++++++++-------------- 1 file changed, 444 insertions(+), 219 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 381b013..d2540a3 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -17,8 +17,7 @@ You should have received a copy of the GNU General Public License along with darktable. If not, see . -]] ---[[ +]] --[[ ULTRAHDR Add a new storage option to generate UltraHDR JPG images. @@ -32,35 +31,59 @@ assuming the first one encountered is the base SDR image, and the second one is The images are merged using libultrahdr example application (ultrahdr_app). ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT -* ultrahdr_app (from https://github.com/google/libultrahdr example dir) +* ultrahdr_app (built using https://github.com/google/libultrahdr/blob/main/docs/building.md instructions) * exiftool +* ffmpeg USAGE * require this file from your main luarc config file -* set exiftool and libultrahdr_app tool paths +* set binary tool paths -This plugin will add a new storage option. - -]] -local dt = require "darktable" +]] local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" local log = require "lib/dtutils.log" local dtsys = require "lib/dtutils.system" +local dd = require "lib/dtutils.debug" local gettext = dt.gettext.gettext -local namespace = 'module_ultrahdr' +local namespace = "module_ultrahdr" -- works with darktable API version from 5.0.0 on du.check_min_api_version("7.0.0", "ultrahdr") -dt.gettext.bindtextdomain("ultrahdr", dt.configuration.config_dir .. "/lua/locale/") +dt.gettext.bindtextdomain(namespace, dt.configuration.config_dir .. "/lua/locale/") local function _(msgid) return gettext(msgid) end --- return data structure for script_manager +local job + +local GUI = { + optionwidgets = { + settings_label = {}, + encoding_variant_combo = {}, + encoding_settings_box = {}, + output_settings_label = {}, + output_settings_box = {}, + use_original_directory = {}, + output_directory_widget = {}, + copy_exif = {}, + import_to_darktable = {}, + metadata_path_label = {}, + metadata_path_widget = {}, + metadata_path_box = {}, + edit_executables_button = {}, + executable_path_widget = {} + }, + options = {}, + run = {} +} + +local flags = {} +flags.event_registered = false -- keep track of whether we've added an event callback or not +flags.module_installed = false -- keep track of whether the module is module_installed local script_data = {} @@ -70,247 +93,449 @@ script_data.metadata = { author = "Krzysztof Kotowicz" } -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, otherwise leave as nil -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 +local PS = dt.configuration.running_os == "windows" and "\\" or "/" +local ENCODING_VARIANT_SDR_AND_GAINMAP = 1 +local ENCODING_VARIANT_SDR_AND_HDR = 2 -local function image_path(image) - return image.path .. "/" .. image.filename +local function save_preferences() + dt.preferences.write(namespace, "encoding_variant", "integer", GUI.optionwidgets.encoding_variant_combo.selected) + if GUI.optionwidgets.metadata_path_widget.value then + dt.preferences.write(namespace, "metadata_path", "string", GUI.optionwidgets.metadata_path_widget.value) + end + dt.preferences.write(namespace, "use_original_directory", "bool", GUI.optionwidgets.use_original_directory.value) + dt.preferences.write(namespace, "output_directory", "string", GUI.optionwidgets.output_directory_widget.value) + dt.preferences.write(namespace, "import_to_darktable", "bool", GUI.optionwidgets.import_to_darktable.value) + dt.preferences.write(namespace, "copy_exif", "bool", GUI.optionwidgets.copy_exif.value) end -local ENCODING_VARIANT_API_4 = 1 +local function load_preferences() + GUI.optionwidgets.encoding_variant_combo.selected = dt.preferences.read(namespace, "encoding_variant", "integer") or + ENCODING_VARIANT_SDR_AND_GAINMAP + GUI.optionwidgets.metadata_path_widget.value = dt.preferences.read(namespace, "metadata_path", "string") + GUI.optionwidgets.use_original_directory.value = dt.preferences.read(namespace, "use_original_directory", "bool") + GUI.optionwidgets.output_directory_widget.value = dt.preferences.read(namespace, "output_directory", "string") + GUI.optionwidgets.import_to_darktable.value = dt.preferences.read(namespace, "import_to_darktable", "bool") + GUI.optionwidgets.copy_exif.value = dt.preferences.read(namespace, "copy_exif", "bool") +end local function get_encoding_variant() - local encoding_variant = dt.preferences.read("ultrahdr", "encoding variant", "integer") - if not encoding_variant then - encoding_variant = ENCODING_VARIANT_API_4 - end - return encoding_variant + return GUI.optionwidgets.encoding_variant_combo.selected end -local function merge_ultrahdr_api4(base, gainmap, ultrahdr_app, exiftool, output) - local metadata = dt.preferences.read("ultrahdr", "metadata path", "string") - if not df.check_if_file_exists(metadata) then - dt.print(_("metadata file not found, did you set the correct path?")) - log.msg(log.error, "metadata file not found.") - return - end - local base_tmp = df.chop_filetype(base) .. ".tmp" - local hdr = df.chop_filetype(base) .. "_hdr." .. df.get_filetype(base) - dtsys.external_command(exiftool .. " -all= " .. df.sanitize_filename(base) .. " -o " .. - df.sanitize_filename(base_tmp)) - dtsys.external_command(exiftool .. " -all= " .. df.sanitize_filename(gainmap) .. " -overwrite_original") - dtsys.external_command(ultrahdr_app .. " -m 0 -i " .. df.sanitize_filename(base_tmp) .. " -g " .. - df.sanitize_filename(gainmap) .. " -f " .. df.sanitize_filename(metadata) .. " -z " .. - df.sanitize_filename(hdr)) - dtsys.external_command(exiftool .. " -tagsfromfile " .. df.sanitize_filename(base) .. " -all>all " .. - df.sanitize_filename(hdr) .. " -overwrite_original") - df.file_move(hdr, df.create_unique_filename(output .. "/" .. df.get_filename(hdr))) - os.remove(base) - os.remove(base_tmp) - os.remove(gainmap) -end +local function assert_settings_correct(encoding_variant) + local settings = { + bin = { + ultrahdr_app = df.check_if_bin_exists("ultrahdr_app"), + exiftool = df.check_if_bin_exists("exiftool"), + ffmpeg = df.check_if_bin_exists("ffmpeg") + }, + output = GUI.optionwidgets.output_directory_widget.value, + use_original_dir = GUI.optionwidgets.use_original_directory.value, + import_to_darktable = GUI.optionwidgets.import_to_darktable.value, + copy_exif = GUI.optionwidgets.copy_exif.value, + metadata = GUI.optionwidgets.metadata_path_widget.value, + tmpdir = dt.configuration.tmp_dir + } -local function assert_settings_correct() - local ultrahdr_app = df.check_if_bin_exists("ultrahdr_app") - log.msg(log.debug, "ultrahdr_app set to ", ultrahdr_app) - local exiftool = df.check_if_bin_exists("exiftool") - log.msg(log.debug, "exiftool set to ", exiftool) - local output = dt.preferences.read("ultrahdr", "output dir", "string") - log.msg(log.debug, "output dir set to ", output) - - if not ultrahdr_app then - dt.print(_("ultrahdr_app is not found, did you set the path?")) - log.msg(log.error, "ultrahdr_app executable not found. Check if the executable is installed.") - log.msg(log.error, "If the executable is installed, check that the path is set correctly.") + if not settings.use_original_dir and not df.check_if_file_exists(settings.output) then + dt.print(string.format(_("output directory (%s) not found, did you set the correct path?"), settings.output)) return end - if not exiftool then - dt.print(_("exiftool is not found, did you set the path?")) - log.msg(log.error, "exiftool executable not found. Check if the executable is installed.") - log.msg(log.error, "If the executable is installed, check that the path is set correctly.") - return + for k, v in pairs(settings.bin) do + if not v then + dt.print(string.format(_("%s is not found, did you set the path?"), k)) + return + end end - return ultrahdr_app, exiftool, output + if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then + if not df.check_if_file_exists(settings.metadata) then + dt.print(_("metadata file not found, did you set the correct path?")) + log.msg(log.error, "metadata file not found.") + return + end + end + + return settings end -local function create_hdr(storage, image_table, extra_data) -- finalize +local function get_stacks(images, encoding_variant) + local stacks = {} + local extra_image_content_type, extra_image_extension + if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then + extra_image_content_type = "gainmap" + elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then + extra_image_extension = "jxl" + extra_image_content_type = "hdr" + end + + local tags = nil + -- Group images into sdr, extra pairs based on their original filename, ignoring the extension + -- Assume that the first encountered image from each filename is an sdr one, unless it has a tag matching the expected extra_image_type, or has the expected extension + for k, v in pairs(images) do + local is_extra = false + tags = dt.tags.get_tags(v) + for ignore, tag in pairs(tags) do + if tag.name == extra_image_content_type then + is_extra = true + end + end + if extra_image_extension and df.get_filetype(v.filename) == extra_image_extension then + is_extra = true + end + -- we assume every image in the stack is generated from the same source image file + local key = df.chop_filetype(v.path .. PS .. v.filename) + if stacks[key] == nil then + stacks[key] = {} + end + if stacks[key]["sdr"] or is_extra then + stacks[key][extra_image_content_type] = v + else + stacks[key]["sdr"] = v + end + end + -- remove invalid stacks + local count = 0 + for k, v in pairs(stacks) do + if not v["sdr"] or not v[extra_image_content_type] then + stacks[k] = nil + elseif (v["sdr"].final_width ~= v[extra_image_content_type].final_width) or + (v["sdr"].final_height ~= v[extra_image_content_type].final_height) then + stacks[k] = nil + elseif extra_image_extension and df.get_filetype(v[extra_image_content_type].filename) ~= extra_image_extension then + stacks[k] = nil + else + count = count + 1 + end + end + return stacks, count +end + +local function stop_job(job) + job.valid = false +end + +local function execute_cmd(cmd) + log.msg(log.debug, cmd) + return dtsys.external_command(cmd) +end + +local function generate_ultrahdr(encoding_variant, images, settings, step, total_steps) + local total_substeps + local substep = 0 + local uhdr + + function update_job_progress() + substep = substep + 1 + if substep > total_substeps then + log.msg(log.debug, + string.format("total_substeps count is too low for encoding_variant %d", encoding_variant)) + end + job.percent = (total_substeps * step + substep) / (total_steps * total_substeps) + end + + if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then + total_substeps = 6 + local msg = string.format(_("Stacking %s"), images["sdr"].filename) + log.msg(log.info, msg) + dt.print(msg) + -- Export both SDR and gainmap to JPEGs + local exporter = dt.new_format("jpeg") + exporter.quality = 95 + local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) .. + ".jpg") + exporter:write_image(images["sdr"], sdr) + local gainmap = df.create_unique_filename(settings.tmpdir .. PS .. images["gainmap"].filename .. "_gainmap.jpg") + exporter:write_image(images["gainmap"], gainmap) + + log.msg(log.debug, string.format(_("Exported files: %s, %s"), sdr, gainmap)) + update_job_progress() + -- Strip EXIFs + execute_cmd(settings.bin.exiftool .. " -all= " .. df.sanitize_filename(sdr) .. " -o " .. + df.sanitize_filename(sdr .. ".noexif")) + execute_cmd(settings.bin.exiftool .. " -all= " .. df.sanitize_filename(gainmap) .. " -overwrite_original") + update_job_progress() + -- Merge files + uhdr = df.chop_filetype(sdr) .. "_ultrahdr.jpg" + + execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -M 0 -i " .. df.sanitize_filename(sdr .. ".noexif") .. " -g " .. + df.sanitize_filename(gainmap) .. " -f " .. df.sanitize_filename(settings.metadata) .. " -z " .. + df.sanitize_filename(uhdr)) + update_job_progress() + -- Copy SDR's EXIF to UltraHDR file + if settings.copy_exif then + execute_cmd(settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -all>all " .. + df.sanitize_filename(uhdr) .. " -overwrite_original -preserve") + end + update_job_progress() + -- Cleanup + os.remove(sdr) + os.remove(sdr .. ".noexif") + os.remove(gainmap) + update_job_progress() + elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then + total_substeps = 5 + -- https://discuss.pixls.us/t/manual-creation-of-ultrahdr-images/45004/20 + -- Step 1: Export SDR to PNG (HDR is already a JPEG-XL) + local exporter = dt.new_format("png") + exporter.bpp = 8 + local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) .. + ".png") + exporter:write_image(images["sdr"], sdr) + uhdr = df.chop_filetype(sdr) .. "_ultrahdr.jpg" + + update_job_progress() + local extra = df.create_unique_filename(settings.tmpdir .. PS .. images["hdr"].filename .. ".raw") + + -- Step 3: Generate libultrahdr RAW images + execute_cmd(settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(sdr) .. " -pix_fmt rgba -f rawvideo " .. + df.sanitize_filename(sdr .. ".raw")) + execute_cmd(settings.bin.ffmpeg .. " -i " .. + df.sanitize_filename(images["hdr"].path .. PS .. images["hdr"].filename) .. + " -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(extra)) + update_job_progress() + execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr .. ".raw") .. " -p " .. + df.sanitize_filename(extra) .. " -a 0 -b 3 -c 1 -C 1 -t 2 -M 1 -s 1 -q 100 -Q 100 -D 1 " .. + " -w " .. tostring(images["sdr"].final_width) .. " -h " .. tostring(images["sdr"].final_height) .. + " -z " .. df.sanitize_filename(uhdr)) + update_job_progress() + if settings.copy_exif then + execute_cmd(settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -all>all " .. + df.sanitize_filename(uhdr) .. " -overwrite_original -preserve") + end + -- Cleanup + os.remove(sdr) + os.remove(sdr .. ".raw") + os.remove(extra) + update_job_progress() + end + + local output_dir = settings.use_original_dir and images["sdr"].path or settings.output + local output_file = df.create_unique_filename(output_dir .. PS .. df.get_filename(uhdr)) + df.file_move(uhdr, output_file) + if settings.import_to_darktable then + local img = dt.database.import(output_file) + -- Add "ultrahdr" tag to the imported image + local tagnr = dt.tags.find("ultrahdr") + if tagnr == nil then + dt.tags.create("ultrahdr") + tagnr = dt.tags.find("ultrahdr") + end + dt.tags.attach(tagnr, img) + end + update_job_progress() +end + +local function main() local saved_log_level = log.log_level() log.log_level(log.info) - local ultrahdr_app, exiftool, output = assert_settings_correct() - if not ultrahdr_app or not exiftool then - return - end + save_preferences() + local encoding_variant = get_encoding_variant() log.msg(log.info, string.format("using encoding variant %d", encoding_variant)) - if encoding_variant == ENCODING_VARIANT_API_4 then - local merged = 0 - for ignore, v in pairs(extra_data) do - if not v then - goto continue - end - local msg = string.format(_("Merging %s and %s"), df.get_filename(image_table[v["base"]]), - df.get_filename(image_table[v["gainmap"]])) - log.msg(log.info, msg) - dt.print(msg) - merge_ultrahdr_api4(image_table[v["base"]], image_table[v["gainmap"]], ultrahdr_app, exiftool, output) - merged = merged + 1 - ::continue:: - end - for ignore, v in pairs(image_table) do - if df.check_if_file_exists(v) then os.remove(v) end - end - dt.print(string.format(_("Created %d UltraHDR image(s) in %s"), merged, output)) - else - local msg = string.format(_("Unknown encoding variant: %d"), encoding_variant) - dt.print_error(msg) - dt.print(msg) + + local settings = assert_settings_correct(encoding_variant) + if not settings then + dt.print(_("Export settings are incorrect, exiting...")) + log.log_level(saved_log_level) + return end + + local images = dt.gui.selection() -- get selected images + if #images < 2 then + dt.print(_("Select at least 2 images to generate UltraHDR image")) + log.log_level(saved_log_level) + return + end + + local stacks, stack_count = get_stacks(images, encoding_variant) + dt.print(string.format(_("Detected %d image stack(s)"), stack_count)) + if stack_count == 0 then + log.log_level(saved_log_level) + return + end + job = dt.gui.create_job(_("Generating UltraHDR images"), true, stop_job) + local count = 0 + for i, v in pairs(stacks) do + generate_ultrahdr(encoding_variant, v, settings, count, stack_count) + count = count + 1 + -- sleep for a short moment to give stop_job callback function a chance to run + dt.control.sleep(10) + end + -- stop job and remove progress_bar from ui, but only if not alreay canceled + if (job.valid) then + job.valid = false + end + log.log_level(saved_log_level) end +GUI.optionwidgets.settings_label = dt.new_widget("section_label") { + label = _("UltraHDR settings") +} + +GUI.optionwidgets.output_settings_label = dt.new_widget("section_label") { + label = _("Output") +} + +GUI.optionwidgets.output_directory_widget = dt.new_widget("file_chooser_button") { + title = _("Select directory to write UltraHDR image files to"), + is_directory = true +} + +GUI.optionwidgets.use_original_directory = dt.new_widget("check_button") { + label = _("Export to original directory"), + tooltip = _("Write UltraHDR images to the same directory as their original images"), + clicked_callback = function(self) + GUI.optionwidgets.output_directory_widget.sensitive = not self.value + end +} + +GUI.optionwidgets.import_to_darktable = dt.new_widget("check_button") { + label = _("Import UltraHDRs to Darktable"), + tooltip = _("Import UltraHDR images to Darktable library after generating, with an 'ultrahdr' tag attached.") +} + +GUI.optionwidgets.copy_exif = dt.new_widget("check_button") { + label = _("Copy EXIF data from SDR file(s)"), + tooltip = _("Copy EXIF data into UltraHDR file(s) from their SDR sources.") +} + +GUI.optionwidgets.output_settings_box = dt.new_widget("box") { + orientation = "vertical", + GUI.optionwidgets.output_settings_label, + GUI.optionwidgets.use_original_directory, + GUI.optionwidgets.output_directory_widget, + GUI.optionwidgets.import_to_darktable, + GUI.optionwidgets.copy_exif +} + +GUI.optionwidgets.metadata_path_label = dt.new_widget("label") { + label = _("ultrahdr_app metadata.cfg file") +} + +GUI.optionwidgets.metadata_path_widget = dt.new_widget("file_chooser_button") { + title = "select libultrahdr metadata path", + is_directory = false +} + +GUI.optionwidgets.metadata_path_box = dt.new_widget("box") { + orientation = "vertical", + GUI.optionwidgets.metadata_path_label, + GUI.optionwidgets.metadata_path_widget +} + +GUI.optionwidgets.encoding_variant_combo = dt.new_widget("combobox") { + label = _("Source images"), + tooltip = string.format(_([[Select types of images in the selection. + +%s: SDR image paired with a monochromatic gain map image +%s: SDR image paired with a JPEG-XL HDR image (10-bit, 'PQ P3 RGB' profile recommended) + +UltraHDR image will be created for each pair of images that: + - have the same underlying image path + filename (ignoring file extension) + - have the same dimensions + + It is assumed that the first image in a pair is the SDR , unless it has a "hdr" / "gainmap" tag. +]]), _("SDR + monochrome gainmap"), _("SDR + JPEG-XL HDR")), + selected = 0, + changed_callback = function(self) + if self.selected == ENCODING_VARIANT_SDR_AND_GAINMAP then + GUI.optionwidgets.metadata_path_box.visible = true + else + GUI.optionwidgets.metadata_path_box.visible = false + end + end, + _("SDR + monochrome gainmap"), -- ENCODING_VARIANT_SDR_AND_GAINMAP, + _("SDR + JPEG-XL HDR") -- ENCODING_VARIANT_SDR_AND_HDR, +} + +GUI.optionwidgets.encoding_settings_box = dt.new_widget("box") { + orientation = "vertical", + GUI.optionwidgets.encoding_variant_combo, + GUI.optionwidgets.metadata_path_box +} + +GUI.optionwidgets.executable_path_widget = df.executable_path_widget({"ultrahdr_app", "exiftool", "ffmpeg"}) +GUI.optionwidgets.executable_path_widget.visible = false + +GUI.optionwidgets.edit_executables_button = dt.new_widget("button") { + label = _("Show / hide executables"), + tooltip = _("Show / hide settings for executable files required for the plugin functionality"), + clicked_callback = function() + GUI.optionwidgets.executable_path_widget.visible = not GUI.optionwidgets.executable_path_widget.visible + end +} + +GUI.options = dt.new_widget("box") { + orientation = "vertical", + GUI.optionwidgets.settings_label, + GUI.optionwidgets.encoding_settings_box, + GUI.optionwidgets.edit_executables_button, + GUI.optionwidgets.executable_path_widget, + GUI.optionwidgets.output_settings_box +} + +GUI.run = dt.new_widget("button") { + label = _("Generate UltraHDR"), + tooltip = _([[Generate UltraHDR image(s) from selection + +Global options in the export module apply to the SDR image. Make sure that a proper color 'profile' setting is used (e.g. Display P3) +]]), + clicked_callback = main +} + +load_preferences() + +local function install_module() + if flags.module_installed then + return + end + dt.register_lib( -- register module + namespace, -- Module name + _("UltraHDR"), -- name + true, -- expandable + true, -- resetable + { + [dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 99} + }, -- containers + dt.new_widget("box") { + orientation = "vertical", + GUI.options, + GUI.run + }, nil, -- view_enter + nil -- view_leave + ) +end + local function destroy() - dt.destroy_storage(namespace) + dt.gui.libs[namespace].visible = false end - - -local function is_supported(storage, format) - local encoding_variant = get_encoding_variant() - if encoding_variant == ENCODING_VARIANT_API_4 then - -- API-4 expects compressed base and gainmap - -- https://github.com/google/libultrahdr/tree/main?tab=readme-ov-file#encoding-api-outline - return format.extension == "jpg" - end - return false +local function restart() + dt.gui.libs[namespace].visible = true end -local function initialize(storage, format, images, high_quality, extra_data) - local saved_log_level = log.log_level() - log.log_level(log.info) - local tags = nil - -- Group images into base, gainmap pairs based on their original filename. - -- Assume that the first encountered image from each filename is a base one, unless it has a "gainmap" tag. - for k, v in pairs(images) do - local has_gainmap_tag = false - tags = dt.tags.get_tags(v) - for ignore, tag in pairs(tags) do - if tag.name == "gainmap" then - has_gainmap_tag = true +if dt.gui.current_view().id == "lighttable" then -- make sure we are in lighttable view + install_module() -- register the lib +else + if not flags.event_registered then -- if we are not in lighttable view then register an event to signal when we might be + -- https://www.darktable.org/lua-api/index.html#darktable_register_event + dt.register_event(namespace, "view-changed", -- we want to be informed when the view changes + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then -- if the view changes from darkroom to lighttable + install_module() -- register the lib end - end - local key = image_path(v) - if extra_data[key] == nil then - extra_data[key] = {} - end - if extra_data[key]["base"] or has_gainmap_tag then - extra_data[key]["gainmap"] = v - else - extra_data[key]["base"] = v - end + end) + flags.event_registered = true -- keep track of whether we have an event handler installed end - -- remove incomplete entries - for k, v in pairs(extra_data) do - if not v["base"] or not v["gainmap"] then - extra_data[k] = nil - end - end - log.log_level(saved_log_level) - return nil end -local function metadata_file_widget() - local box_widgets = {} - table.insert(box_widgets, dt.new_widget("label") { - label = "libultrahdr metadata file" - }) - local path = dt.preferences.read("ultrahdr", "metadata path", "string") - if not path then - path = "" - end - table.insert(box_widgets, dt.new_widget("file_chooser_button") { - title = "select libultrahdr metadata path", - value = path, - is_directory = false, - changed_callback = function(self) - if df.check_if_file_exists(self.value) then - dt.preferences.write("ultrahdr", "metadata path", "string", self.value) - end - end - }) - - local box = dt.new_widget("box") { - orientation = "vertical", - table.unpack(box_widgets) - } - return box -end - -local function output_directory_widget() - local box_widgets = {} - table.insert(box_widgets, dt.new_widget("label") { - label = "output directory" - }) - local path = dt.preferences.read("ultrahdr", "output dir", "string") - if not path then - path = "" - end - table.insert(box_widgets, dt.new_widget("file_chooser_button") { - title = "select libultrahdr metadata path", - value = path, - is_directory = true, - changed_callback = function(self) - dt.preferences.write("ultrahdr", "output dir", "string", self.value) - end - }) - - local box = dt.new_widget("box") { - orientation = "vertical", - table.unpack(box_widgets) - } - return box -end - -local function encoding_variant_widget() - local metadata_widget = metadata_file_widget() - local encoding_variant = get_encoding_variant() - local combobox = dt.new_widget("combobox"){ - label = _("Generate HDR from"), - selected = encoding_variant, - changed_callback=function(self) - dt.preferences.write("ultrahdr", "encoding variant", "integer", self.selected) - if self.selected == ENCODING_VARIANT_API_4 then - metadata_widget.visible = true - else - metadata_widget.visible = false - end - end, - _("SDR + gainmap (API-4)") -- ENCODING_VARIANT_API_4, - } - - combobox.changed_callback(combobox) - return dt.new_widget("box") { - orientation = "vertical", - combobox, - metadata_widget - } -end - -local ultrahdr_widget = dt.new_widget("box") { - orientation = "vertical", - output_directory_widget(), - encoding_variant_widget(), - df.executable_path_widget({"ultrahdr_app", "exiftool"}) -} - --- Register - -dt.register_storage(namespace, _("UltraHDR JPEG"), nil, create_hdr, is_supported, initialize, ultrahdr_widget) - script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = restart return script_data From 865d29fb4084af6e1eef15d5b5f8fe33a27c3c15 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Thu, 19 Sep 2024 13:13:47 +0200 Subject: [PATCH 10/41] Changed the UltraHDR JPEG quality to 95. --- contrib/ultrahdr.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index d2540a3..0d0e34b 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -296,7 +296,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total " -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(extra)) update_job_progress() execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr .. ".raw") .. " -p " .. - df.sanitize_filename(extra) .. " -a 0 -b 3 -c 1 -C 1 -t 2 -M 1 -s 1 -q 100 -Q 100 -D 1 " .. + df.sanitize_filename(extra) .. " -a 0 -b 3 -c 1 -C 1 -t 2 -M 1 -s 1 -q 95 -Q 95 -D 1 " .. " -w " .. tostring(images["sdr"].final_width) .. " -h " .. tostring(images["sdr"].final_height) .. " -z " .. df.sanitize_filename(uhdr)) update_job_progress() From 28f87566e1522432856911bee9862136c095ed7c Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Thu, 19 Sep 2024 15:11:52 +0200 Subject: [PATCH 11/41] Added better messaging. --- contrib/ultrahdr.lua | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 0d0e34b..2efeb2c 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -237,9 +237,6 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then total_substeps = 6 - local msg = string.format(_("Stacking %s"), images["sdr"].filename) - log.msg(log.info, msg) - dt.print(msg) -- Export both SDR and gainmap to JPEGs local exporter = dt.new_format("jpeg") exporter.quality = 95 @@ -324,6 +321,10 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total end dt.tags.attach(tagnr, img) end + + local msg = string.format(_("Generated %s."), df.get_filename(output_file)) + log.msg(log.info, msg) + dt.print(msg) update_job_progress() end @@ -369,6 +370,9 @@ local function main() job.valid = false end + local msg = string.format(_("Generated %d UltraHDR image(s)."), count) + log.msg(log.info, msg) + dt.print(msg) log.log_level(saved_log_level) end From 496f8e704dc22a9d9f01c1aa6090582152a72a65 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Thu, 19 Sep 2024 17:09:54 +0200 Subject: [PATCH 12/41] Fixed doc. --- contrib/ultrahdr.lua | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 2efeb2c..628600a 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -20,14 +20,10 @@ ]] --[[ ULTRAHDR -Add a new storage option to generate UltraHDR JPG images. +Generate UltraHDR JPG images from various combinations of source files (SDR, HDR, gainmap). https://developer.android.com/media/platform/hdr-image-format -Of all exported files, the storage detects pairs of files generated from the same source image, -assuming the first one encountered is the base SDR image, and the second one is the gainmap -(alternatively, you can tag the gainmaps with a "gainmap" tag). - The images are merged using libultrahdr example application (ultrahdr_app). ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT From 1a3a7f17ff53a0e33b3958134e31fb093965c2bb Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Thu, 19 Sep 2024 17:54:15 +0200 Subject: [PATCH 13/41] Added SDR + auto gainmap generation encoding variant. --- contrib/ultrahdr.lua | 82 +++++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 628600a..ee92c99 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -92,6 +92,7 @@ script_data.metadata = { local PS = dt.configuration.running_os == "windows" and "\\" or "/" local ENCODING_VARIANT_SDR_AND_GAINMAP = 1 local ENCODING_VARIANT_SDR_AND_HDR = 2 +local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3 local function save_preferences() dt.preferences.write(namespace, "encoding_variant", "integer", GUI.optionwidgets.encoding_variant_combo.selected) @@ -128,7 +129,7 @@ local function assert_settings_correct(encoding_variant) output = GUI.optionwidgets.output_directory_widget.value, use_original_dir = GUI.optionwidgets.use_original_directory.value, import_to_darktable = GUI.optionwidgets.import_to_darktable.value, - copy_exif = GUI.optionwidgets.copy_exif.value, + copy_exif = GUI.optionwidgets.copy_exif.value, metadata = GUI.optionwidgets.metadata_path_widget.value, tmpdir = dt.configuration.tmp_dir } @@ -164,6 +165,8 @@ local function get_stacks(images, encoding_variant) elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then extra_image_extension = "jxl" extra_image_content_type = "hdr" + elseif encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then + extra_image_content_type = nil end local tags = nil @@ -173,7 +176,7 @@ local function get_stacks(images, encoding_variant) local is_extra = false tags = dt.tags.get_tags(v) for ignore, tag in pairs(tags) do - if tag.name == extra_image_content_type then + if extra_image_content_type and tag.name == extra_image_content_type then is_extra = true end end @@ -185,23 +188,27 @@ local function get_stacks(images, encoding_variant) if stacks[key] == nil then stacks[key] = {} end - if stacks[key]["sdr"] or is_extra then + if extra_image_content_type and (stacks[key]["sdr"] or is_extra) then stacks[key][extra_image_content_type] = v - else + elseif not is_extra then stacks[key]["sdr"] = v end end -- remove invalid stacks local count = 0 for k, v in pairs(stacks) do - if not v["sdr"] or not v[extra_image_content_type] then - stacks[k] = nil - elseif (v["sdr"].final_width ~= v[extra_image_content_type].final_width) or - (v["sdr"].final_height ~= v[extra_image_content_type].final_height) then - stacks[k] = nil - elseif extra_image_extension and df.get_filetype(v[extra_image_content_type].filename) ~= extra_image_extension then - stacks[k] = nil - else + if extra_image_content_type then + if not v["sdr"] or not v[extra_image_content_type] then + stacks[k] = nil + elseif (v["sdr"].final_width ~= v[extra_image_content_type].final_width) or + (v["sdr"].final_height ~= v[extra_image_content_type].final_height) then + stacks[k] = nil + elseif extra_image_extension and df.get_filetype(v[extra_image_content_type].filename) ~= + extra_image_extension then + stacks[k] = nil + end + end + if stacks[k] then count = count + 1 end end @@ -231,7 +238,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total job.percent = (total_substeps * step + substep) / (total_steps * total_substeps) end - if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then + if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then total_substeps = 6 -- Export both SDR and gainmap to JPEGs local exporter = dt.new_format("jpeg") @@ -239,20 +246,27 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) .. ".jpg") exporter:write_image(images["sdr"], sdr) - local gainmap = df.create_unique_filename(settings.tmpdir .. PS .. images["gainmap"].filename .. "_gainmap.jpg") - exporter:write_image(images["gainmap"], gainmap) - + local gainmap + if encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then -- SDR is also a gainmap + gainmap = sdr + else + gainmap = df.create_unique_filename(settings.tmpdir .. PS .. images["gainmap"].filename .. "_gainmap.jpg") + exporter:write_image(images["gainmap"], gainmap) + end + dd.dprint(images["gainmap"]) log.msg(log.debug, string.format(_("Exported files: %s, %s"), sdr, gainmap)) update_job_progress() -- Strip EXIFs execute_cmd(settings.bin.exiftool .. " -all= " .. df.sanitize_filename(sdr) .. " -o " .. df.sanitize_filename(sdr .. ".noexif")) - execute_cmd(settings.bin.exiftool .. " -all= " .. df.sanitize_filename(gainmap) .. " -overwrite_original") + if sdr ~= gainmap then + execute_cmd(settings.bin.exiftool .. " -all= " .. df.sanitize_filename(gainmap) .. " -overwrite_original") + end update_job_progress() -- Merge files uhdr = df.chop_filetype(sdr) .. "_ultrahdr.jpg" - execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -M 0 -i " .. df.sanitize_filename(sdr .. ".noexif") .. " -g " .. + execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -i " .. df.sanitize_filename(sdr .. ".noexif") .. " -g " .. df.sanitize_filename(gainmap) .. " -f " .. df.sanitize_filename(settings.metadata) .. " -z " .. df.sanitize_filename(uhdr)) update_job_progress() @@ -265,7 +279,9 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total -- Cleanup os.remove(sdr) os.remove(sdr .. ".noexif") - os.remove(gainmap) + if sdr ~= gainmap then + os.remove(gainmap) + end update_job_progress() elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then total_substeps = 5 @@ -289,9 +305,9 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total " -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(extra)) update_job_progress() execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr .. ".raw") .. " -p " .. - df.sanitize_filename(extra) .. " -a 0 -b 3 -c 1 -C 1 -t 2 -M 1 -s 1 -q 95 -Q 95 -D 1 " .. - " -w " .. tostring(images["sdr"].final_width) .. " -h " .. tostring(images["sdr"].final_height) .. - " -z " .. df.sanitize_filename(uhdr)) + df.sanitize_filename(extra) .. " -a 0 -b 3 -c 1 -C 1 -t 2 -M 1 -s 1 -q 95 -Q 95 -D 1 " .. " -w " .. + tostring(images["sdr"].final_width) .. " -h " .. tostring(images["sdr"].final_height) .. " -z " .. + df.sanitize_filename(uhdr)) update_job_progress() if settings.copy_exif then execute_cmd(settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -all>all " .. @@ -320,7 +336,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total local msg = string.format(_("Generated %s."), df.get_filename(output_file)) log.msg(log.info, msg) - dt.print(msg) + dt.print(msg) update_job_progress() end @@ -340,14 +356,7 @@ local function main() return end - local images = dt.gui.selection() -- get selected images - if #images < 2 then - dt.print(_("Select at least 2 images to generate UltraHDR image")) - log.log_level(saved_log_level) - return - end - - local stacks, stack_count = get_stacks(images, encoding_variant) + local stacks, stack_count = get_stacks(dt.gui.selection(), encoding_variant) dt.print(string.format(_("Detected %d image stack(s)"), stack_count)) if stack_count == 0 then log.log_level(saved_log_level) @@ -433,13 +442,15 @@ GUI.optionwidgets.encoding_variant_combo = dt.new_widget("combobox") { %s: SDR image paired with a monochromatic gain map image %s: SDR image paired with a JPEG-XL HDR image (10-bit, 'PQ P3 RGB' profile recommended) +%s: SDR image only. Gainmaps will be just copies of SDR images. UltraHDR image will be created for each pair of images that: - have the same underlying image path + filename (ignoring file extension) - have the same dimensions - It is assumed that the first image in a pair is the SDR , unless it has a "hdr" / "gainmap" tag. -]]), _("SDR + monochrome gainmap"), _("SDR + JPEG-XL HDR")), +By default, the first image in a pair is treated as SDR, and the next one store extra gainmap/HDR data. +You can force the image into a specific slot by attaching "hdr" / "gainmap" tags. +]]), _("SDR + monochrome gainmap"), _("SDR + JPEG-XL HDR"), _("SDR (auto gainmap)")), selected = 0, changed_callback = function(self) if self.selected == ENCODING_VARIANT_SDR_AND_GAINMAP then @@ -448,8 +459,9 @@ UltraHDR image will be created for each pair of images that: GUI.optionwidgets.metadata_path_box.visible = false end end, - _("SDR + monochrome gainmap"), -- ENCODING_VARIANT_SDR_AND_GAINMAP, - _("SDR + JPEG-XL HDR") -- ENCODING_VARIANT_SDR_AND_HDR, + _("SDR + monochrome gainmap"), -- ENCODING_VARIANT_SDR_AND_GAINMAP + _("SDR + JPEG-XL HDR"), -- ENCODING_VARIANT_SDR_AND_HDR + _("SDR (auto gainmap)") -- ENCODING_VARIANT_SDR_AUTO_GAINMAP } GUI.optionwidgets.encoding_settings_box = dt.new_widget("box") { From 794b12305c4173b0dfa796975c6a1cd5339335ae Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Thu, 19 Sep 2024 18:07:25 +0200 Subject: [PATCH 14/41] Fixed tooltip language. --- contrib/ultrahdr.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index ee92c99..3b3f557 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -440,15 +440,15 @@ GUI.optionwidgets.encoding_variant_combo = dt.new_widget("combobox") { label = _("Source images"), tooltip = string.format(_([[Select types of images in the selection. -%s: SDR image paired with a monochromatic gain map image -%s: SDR image paired with a JPEG-XL HDR image (10-bit, 'PQ P3 RGB' profile recommended) -%s: SDR image only. Gainmaps will be just copies of SDR images. +- %s: SDR image paired with a monochromatic gain map image +- %s: SDR image paired with a JPEG-XL HDR image (10-bit, 'PQ P3 RGB' profile recommended) +- %s: SDR images only. Gainmaps will be copies of SDR images (the simplest option). UltraHDR image will be created for each pair of images that: - have the same underlying image path + filename (ignoring file extension) - have the same dimensions -By default, the first image in a pair is treated as SDR, and the next one store extra gainmap/HDR data. +By default, the first image in a pair is treated as SDR, and the second one stores extra gainmap/HDR data. You can force the image into a specific slot by attaching "hdr" / "gainmap" tags. ]]), _("SDR + monochrome gainmap"), _("SDR + JPEG-XL HDR"), _("SDR (auto gainmap)")), selected = 0, From fe6d262d9b2e078394c5e11972532aa133799bd5 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Fri, 20 Sep 2024 09:29:49 +0200 Subject: [PATCH 15/41] Display all error messages when checking configuration. Changed config defaults to require less user input (use_original_directory=TRUE, import_to_darktable=TRUE). Account for nil values when checking for file existence. --- contrib/ultrahdr.lua | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 3b3f557..4e55cfc 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -100,7 +100,9 @@ local function save_preferences() dt.preferences.write(namespace, "metadata_path", "string", GUI.optionwidgets.metadata_path_widget.value) end dt.preferences.write(namespace, "use_original_directory", "bool", GUI.optionwidgets.use_original_directory.value) - dt.preferences.write(namespace, "output_directory", "string", GUI.optionwidgets.output_directory_widget.value) + if GUI.optionwidgets.output_directory_widget.value then + dt.preferences.write(namespace, "output_directory", "string", GUI.optionwidgets.output_directory_widget.value) + end dt.preferences.write(namespace, "import_to_darktable", "bool", GUI.optionwidgets.import_to_darktable.value) dt.preferences.write(namespace, "copy_exif", "bool", GUI.optionwidgets.copy_exif.value) end @@ -108,11 +110,11 @@ end local function load_preferences() GUI.optionwidgets.encoding_variant_combo.selected = dt.preferences.read(namespace, "encoding_variant", "integer") or ENCODING_VARIANT_SDR_AND_GAINMAP - GUI.optionwidgets.metadata_path_widget.value = dt.preferences.read(namespace, "metadata_path", "string") - GUI.optionwidgets.use_original_directory.value = dt.preferences.read(namespace, "use_original_directory", "bool") - GUI.optionwidgets.output_directory_widget.value = dt.preferences.read(namespace, "output_directory", "string") - GUI.optionwidgets.import_to_darktable.value = dt.preferences.read(namespace, "import_to_darktable", "bool") - GUI.optionwidgets.copy_exif.value = dt.preferences.read(namespace, "copy_exif", "bool") + GUI.optionwidgets.metadata_path_widget.value = dt.preferences.read(namespace, "metadata_path", "string") or "" + GUI.optionwidgets.use_original_directory.value = dt.preferences.read(namespace, "use_original_directory", "bool") or true + GUI.optionwidgets.output_directory_widget.value = dt.preferences.read(namespace, "output_directory", "string") or "" + GUI.optionwidgets.import_to_darktable.value = dt.preferences.read(namespace, "import_to_darktable", "bool") or true + GUI.optionwidgets.copy_exif.value = dt.preferences.read(namespace, "copy_exif", "bool") or false end local function get_encoding_variant() @@ -120,6 +122,7 @@ local function get_encoding_variant() end local function assert_settings_correct(encoding_variant) + local errors = {} local settings = { bin = { ultrahdr_app = df.check_if_bin_exists("ultrahdr_app"), @@ -134,27 +137,26 @@ local function assert_settings_correct(encoding_variant) tmpdir = dt.configuration.tmp_dir } - if not settings.use_original_dir and not df.check_if_file_exists(settings.output) then - dt.print(string.format(_("output directory (%s) not found, did you set the correct path?"), settings.output)) - return + if not settings.use_original_dir and (not settings.output or not df.check_if_file_exists(settings.output)) then + table.insert(errors, string.format(_("output directory (%s) not found"), settings.output)) end for k, v in pairs(settings.bin) do if not v then - dt.print(string.format(_("%s is not found, did you set the path?"), k)) - return + table.insert(errors, string.format(_("%s binary not found"), k)) end end if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then - if not df.check_if_file_exists(settings.metadata) then - dt.print(_("metadata file not found, did you set the correct path?")) - log.msg(log.error, "metadata file not found.") - return + if not settings.metadata or not df.check_if_file_exists(settings.metadata) then + table.insert(errors, _("metadata.cfg file not found (select one from libultrahdr/examples directory)")) end end - return settings + if #errors > 0 then + return nil, errors + end + return settings, nil end local function get_stacks(images, encoding_variant) @@ -253,7 +255,6 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total gainmap = df.create_unique_filename(settings.tmpdir .. PS .. images["gainmap"].filename .. "_gainmap.jpg") exporter:write_image(images["gainmap"], gainmap) end - dd.dprint(images["gainmap"]) log.msg(log.debug, string.format(_("Exported files: %s, %s"), sdr, gainmap)) update_job_progress() -- Strip EXIFs @@ -349,9 +350,9 @@ local function main() local encoding_variant = get_encoding_variant() log.msg(log.info, string.format("using encoding variant %d", encoding_variant)) - local settings = assert_settings_correct(encoding_variant) + local settings, errors = assert_settings_correct(encoding_variant) if not settings then - dt.print(_("Export settings are incorrect, exiting...")) + dt.print(string.format(_("Export settings are incorrect, exiting:\n\n%s"), table.concat(errors, "\n"))) log.log_level(saved_log_level) return end @@ -453,6 +454,7 @@ You can force the image into a specific slot by attaching "hdr" / "gainmap" tags ]]), _("SDR + monochrome gainmap"), _("SDR + JPEG-XL HDR"), _("SDR (auto gainmap)")), selected = 0, changed_callback = function(self) + GUI.run.sensitive = self.selected and self.selected > 0 if self.selected == ENCODING_VARIANT_SDR_AND_GAINMAP then GUI.optionwidgets.metadata_path_box.visible = true else From 2e95c4837bc06fe27b04f63dd4d31ee20ca4caef Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Fri, 20 Sep 2024 11:16:28 +0200 Subject: [PATCH 16/41] Added option to generate metadata file. Fixed pref load / saving code defaults. Metadata file is required in ENCODING_VARIANT_SDR_AUTO_GAINMAP mode. --- contrib/ultrahdr.lua | 78 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 4e55cfc..5f0ca6f 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -67,9 +67,11 @@ local GUI = { output_directory_widget = {}, copy_exif = {}, import_to_darktable = {}, + metadata_file_generate = {}, metadata_path_label = {}, metadata_path_widget = {}, metadata_path_box = {}, + metadata_path_box2 = {}, edit_executables_button = {}, executable_path_widget = {} }, @@ -94,6 +96,26 @@ local ENCODING_VARIANT_SDR_AND_GAINMAP = 1 local ENCODING_VARIANT_SDR_AND_HDR = 2 local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3 +local function generate_metadata_file() + local default_metadata_file = [[--maxContentBoost 6.0 +--minContentBoost 1.0 +--gamma 1.0 +--offsetSdr 0.0 +--offsetHdr 0.0 +--hdrCapacityMin 1.0 +--hdrCapacityMax 6.0]] + + local filename = dt.configuration.config_dir .. PS .. "ultrahdr_metadata.cfg" + local f, err = io.open(filename, "w+") + if not f then + dt.print(err) + return nil + end + f:write(default_metadata_file) + f:close() + return filename +end + local function save_preferences() dt.preferences.write(namespace, "encoding_variant", "integer", GUI.optionwidgets.encoding_variant_combo.selected) if GUI.optionwidgets.metadata_path_widget.value then @@ -108,13 +130,22 @@ local function save_preferences() end local function load_preferences() - GUI.optionwidgets.encoding_variant_combo.selected = dt.preferences.read(namespace, "encoding_variant", "integer") or - ENCODING_VARIANT_SDR_AND_GAINMAP - GUI.optionwidgets.metadata_path_widget.value = dt.preferences.read(namespace, "metadata_path", "string") or "" - GUI.optionwidgets.use_original_directory.value = dt.preferences.read(namespace, "use_original_directory", "bool") or true - GUI.optionwidgets.output_directory_widget.value = dt.preferences.read(namespace, "output_directory", "string") or "" - GUI.optionwidgets.import_to_darktable.value = dt.preferences.read(namespace, "import_to_darktable", "bool") or true - GUI.optionwidgets.copy_exif.value = dt.preferences.read(namespace, "copy_exif", "bool") or false + -- Since the option #1 is the default, and empty numeric prefs are 0, we can use math.max + GUI.optionwidgets.encoding_variant_combo.selected = math.max( + dt.preferences.read(namespace, "encoding_variant", "integer"), ENCODING_VARIANT_SDR_AND_GAINMAP) + GUI.optionwidgets.metadata_path_widget.value = dt.preferences.read(namespace, "metadata_path", "string") + if not GUI.optionwidgets.metadata_path_widget.value then -- file widgets reset "" value to nil + GUI.optionwidgets.metadata_path_widget.value = generate_metadata_file() + -- Avoid regenerating the file if the user only enables the plugin, but never clicks "Generate" + dt.preferences.write(namespace, "metadata_path", "string", GUI.optionwidgets.metadata_path_widget.value) + end + GUI.optionwidgets.output_directory_widget.value = dt.preferences.read(namespace, "output_directory", "string") + GUI.optionwidgets.use_original_directory.value = dt.preferences.read(namespace, "use_original_directory", "bool") + if not GUI.optionwidgets.output_directory_widget.value then + GUI.optionwidgets.use_original_directory.value = true + end + GUI.optionwidgets.import_to_darktable.value = dt.preferences.read(namespace, "import_to_darktable", "bool") + GUI.optionwidgets.copy_exif.value = dt.preferences.read(namespace, "copy_exif", "bool") end local function get_encoding_variant() @@ -147,9 +178,9 @@ local function assert_settings_correct(encoding_variant) end end - if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then + if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then if not settings.metadata or not df.check_if_file_exists(settings.metadata) then - table.insert(errors, _("metadata.cfg file not found (select one from libultrahdr/examples directory)")) + table.insert(errors, _("metadata.cfg file not found (use 'Generate' button)")) end end @@ -423,25 +454,40 @@ GUI.optionwidgets.output_settings_box = dt.new_widget("box") { } GUI.optionwidgets.metadata_path_label = dt.new_widget("label") { - label = _("ultrahdr_app metadata.cfg file") + label = _("Gainmap metadata file") } GUI.optionwidgets.metadata_path_widget = dt.new_widget("file_chooser_button") { - title = "select libultrahdr metadata path", + title = _("Select libultrahdr metadata.cfg file"), is_directory = false } +GUI.optionwidgets.metadata_file_generate = dt.new_widget("button") { + label = _("Generate"), + tooltip = _("Generate new metadata file with default values"), + clicked_callback = function() + local metadata_file = generate_metadata_file() + GUI.optionwidgets.metadata_path_widget.value = metadata_file + end +} + +GUI.optionwidgets.metadata_path_box2 = dt.new_widget("box") { + orientation = "horizontal", + GUI.optionwidgets.metadata_path_widget, + GUI.optionwidgets.metadata_file_generate +} + GUI.optionwidgets.metadata_path_box = dt.new_widget("box") { orientation = "vertical", GUI.optionwidgets.metadata_path_label, - GUI.optionwidgets.metadata_path_widget + GUI.optionwidgets.metadata_path_box2 } GUI.optionwidgets.encoding_variant_combo = dt.new_widget("combobox") { label = _("Source images"), tooltip = string.format(_([[Select types of images in the selection. -- %s: SDR image paired with a monochromatic gain map image +- %s: SDR image paired with a gain map image - %s: SDR image paired with a JPEG-XL HDR image (10-bit, 'PQ P3 RGB' profile recommended) - %s: SDR images only. Gainmaps will be copies of SDR images (the simplest option). @@ -451,17 +497,17 @@ UltraHDR image will be created for each pair of images that: By default, the first image in a pair is treated as SDR, and the second one stores extra gainmap/HDR data. You can force the image into a specific slot by attaching "hdr" / "gainmap" tags. -]]), _("SDR + monochrome gainmap"), _("SDR + JPEG-XL HDR"), _("SDR (auto gainmap)")), +]]), _("SDR + gainmap"), _("SDR + JPEG-XL HDR"), _("SDR (auto gainmap)")), selected = 0, changed_callback = function(self) GUI.run.sensitive = self.selected and self.selected > 0 - if self.selected == ENCODING_VARIANT_SDR_AND_GAINMAP then + if self.selected == ENCODING_VARIANT_SDR_AND_GAINMAP or self.selected == ENCODING_VARIANT_SDR_AUTO_GAINMAP then GUI.optionwidgets.metadata_path_box.visible = true else GUI.optionwidgets.metadata_path_box.visible = false end end, - _("SDR + monochrome gainmap"), -- ENCODING_VARIANT_SDR_AND_GAINMAP + _("SDR + gainmap"), -- ENCODING_VARIANT_SDR_AND_GAINMAP _("SDR + JPEG-XL HDR"), -- ENCODING_VARIANT_SDR_AND_HDR _("SDR (auto gainmap)") -- ENCODING_VARIANT_SDR_AUTO_GAINMAP } From 7e8340c526a264a98efcd2917a9e5e1663fcc04a Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Fri, 20 Sep 2024 11:38:24 +0200 Subject: [PATCH 17/41] Don't overwrite existing stack slots (that way the first matching image in a stack will be used, vs the last one). --- contrib/ultrahdr.lua | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 5f0ca6f..cba1cc9 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -216,15 +216,21 @@ local function get_stacks(images, encoding_variant) if extra_image_extension and df.get_filetype(v.filename) == extra_image_extension then is_extra = true end - -- we assume every image in the stack is generated from the same source image file + -- We assume every image in the stack is generated from the same source image file local key = df.chop_filetype(v.path .. PS .. v.filename) if stacks[key] == nil then stacks[key] = {} end - if extra_image_content_type and (stacks[key]["sdr"] or is_extra) then - stacks[key][extra_image_content_type] = v + if extra_image_content_type and (is_extra or stacks[key]["sdr"]) then + -- Don't overwrite existing entries + if not stacks[key][extra_image_content_type] then + stacks[key][extra_image_content_type] = v + end elseif not is_extra then - stacks[key]["sdr"] = v + -- Don't overwrite existing entries + if not stacks[key]["sdr"] then + stacks[key]["sdr"] = v + end end end -- remove invalid stacks From 99c3548950533de135bbcd2cb5e67585341b0030 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Sat, 21 Sep 2024 07:54:45 +0200 Subject: [PATCH 18/41] Copy only EXIF tags to UltraHDR. --- contrib/ultrahdr.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index cba1cc9..326b098 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -310,7 +310,9 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total update_job_progress() -- Copy SDR's EXIF to UltraHDR file if settings.copy_exif then - execute_cmd(settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -all>all " .. + -- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all). + -- This might hapen e.g. when the source files are Adobe gainmap HDRs. + execute_cmd(settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -exif " .. df.sanitize_filename(uhdr) .. " -overwrite_original -preserve") end update_job_progress() @@ -348,7 +350,9 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total df.sanitize_filename(uhdr)) update_job_progress() if settings.copy_exif then - execute_cmd(settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -all>all " .. + -- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all). + -- This might hapen e.g. when the source files are Adobe gainmap HDRs. + execute_cmd(settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -exif " .. df.sanitize_filename(uhdr) .. " -overwrite_original -preserve") end -- Cleanup From 39f63cc8c59441fe2557be469bbf9090c642bcd6 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Sat, 21 Sep 2024 09:44:56 +0200 Subject: [PATCH 19/41] Fallback to reading image's width/height, as sometimes final_width/height is 0. --- contrib/ultrahdr.lua | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 326b098..2ccf2eb 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -190,7 +190,14 @@ local function assert_settings_correct(encoding_variant) return settings, nil end -local function get_stacks(images, encoding_variant) +local function get_dimensions(image) + if image.final_width > 0 then + return image.final_width, image.final_height + end + return image.width, image.height +end + +local function get_stacks(images, encoding_variant, selection_type) local stacks = {} local extra_image_content_type, extra_image_extension if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then @@ -239,12 +246,15 @@ local function get_stacks(images, encoding_variant) if extra_image_content_type then if not v["sdr"] or not v[extra_image_content_type] then stacks[k] = nil - elseif (v["sdr"].final_width ~= v[extra_image_content_type].final_width) or - (v["sdr"].final_height ~= v[extra_image_content_type].final_height) then - stacks[k] = nil elseif extra_image_extension and df.get_filetype(v[extra_image_content_type].filename) ~= extra_image_extension then stacks[k] = nil + else + local sdr_w, sdr_h = get_dimensions(v["sdr"]) + local extra_w, extra_h = get_dimensions(v[extra_image_content_type]) + if (sdr_w ~= extra_w) or (sdr_h ~= extra_h) then + stacks[k] = nil + end end end if stacks[k] then @@ -344,9 +354,10 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total df.sanitize_filename(images["hdr"].path .. PS .. images["hdr"].filename) .. " -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(extra)) update_job_progress() + local sdr_w, sdr_h = get_dimensions(images["sdr"]) execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr .. ".raw") .. " -p " .. df.sanitize_filename(extra) .. " -a 0 -b 3 -c 1 -C 1 -t 2 -M 1 -s 1 -q 95 -Q 95 -D 1 " .. " -w " .. - tostring(images["sdr"].final_width) .. " -h " .. tostring(images["sdr"].final_height) .. " -z " .. + tostring(sdr_w) .. " -h " .. tostring(sdr_h) .. " -z " .. df.sanitize_filename(uhdr)) update_job_progress() if settings.copy_exif then From 08726427aab2afc80d34743d98b06de48eb8371b Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Sat, 21 Sep 2024 09:47:08 +0200 Subject: [PATCH 20/41] Don't re-export JPEG files if their images are not altered. --- contrib/ultrahdr.lua | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 2ccf2eb..c6cf9dc 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -287,20 +287,28 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total job.percent = (total_substeps * step + substep) / (total_steps * total_substeps) end + function copy_or_export_jpg(src, dest) + if df.get_filetype(src.filename) == "jpg" and not src.is_altered then + df.file_copy(src.path .. PS .. src.filename, dest) + else + local exporter = dt.new_format("jpeg") + exporter.quality = 95 + exporter:write_image(src, dest) + end + end + if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then total_substeps = 6 - -- Export both SDR and gainmap to JPEGs - local exporter = dt.new_format("jpeg") - exporter.quality = 95 + -- Export/copy both SDR and gainmap to JPEGs local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) .. ".jpg") - exporter:write_image(images["sdr"], sdr) + copy_or_export_jpg(images["sdr"], sdr) local gainmap if encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then -- SDR is also a gainmap gainmap = sdr else gainmap = df.create_unique_filename(settings.tmpdir .. PS .. images["gainmap"].filename .. "_gainmap.jpg") - exporter:write_image(images["gainmap"], gainmap) + copy_or_export_jpg(images["gainmap"], gainmap) end log.msg(log.debug, string.format(_("Exported files: %s, %s"), sdr, gainmap)) update_job_progress() From 44e442e7eb508f90c2a31c45cd5657fb7b487fa0 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Sat, 21 Sep 2024 09:49:06 +0200 Subject: [PATCH 21/41] Support grouping images into stacks by different criteria (or forcing the selection to be interpreted as a single UltraHDR stack). --- contrib/ultrahdr.lua | 68 ++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index c6cf9dc..c4fadde 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -60,6 +60,7 @@ local GUI = { optionwidgets = { settings_label = {}, encoding_variant_combo = {}, + selection_type_combo = {}, encoding_settings_box = {}, output_settings_label = {}, output_settings_box = {}, @@ -96,6 +97,9 @@ local ENCODING_VARIANT_SDR_AND_GAINMAP = 1 local ENCODING_VARIANT_SDR_AND_HDR = 2 local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3 +local SELECTION_TYPE_ONE_STACK = 1 +local SELECTION_TYPE_GROUP_BY_FNAME = 2 + local function generate_metadata_file() local default_metadata_file = [[--maxContentBoost 6.0 --minContentBoost 1.0 @@ -118,6 +122,7 @@ end local function save_preferences() dt.preferences.write(namespace, "encoding_variant", "integer", GUI.optionwidgets.encoding_variant_combo.selected) + dt.preferences.write(namespace, "selection_type", "integer", GUI.optionwidgets.selection_type_combo.selected) if GUI.optionwidgets.metadata_path_widget.value then dt.preferences.write(namespace, "metadata_path", "string", GUI.optionwidgets.metadata_path_widget.value) end @@ -133,6 +138,9 @@ local function load_preferences() -- Since the option #1 is the default, and empty numeric prefs are 0, we can use math.max GUI.optionwidgets.encoding_variant_combo.selected = math.max( dt.preferences.read(namespace, "encoding_variant", "integer"), ENCODING_VARIANT_SDR_AND_GAINMAP) + GUI.optionwidgets.selection_type_combo.selected = math.max( + dt.preferences.read(namespace, "selection_type", "integer"), SELECTION_TYPE_ONE_STACK) + GUI.optionwidgets.metadata_path_widget.value = dt.preferences.read(namespace, "metadata_path", "string") if not GUI.optionwidgets.metadata_path_widget.value then -- file widgets reset "" value to nil GUI.optionwidgets.metadata_path_widget.value = generate_metadata_file() @@ -148,10 +156,6 @@ local function load_preferences() GUI.optionwidgets.copy_exif.value = dt.preferences.read(namespace, "copy_exif", "bool") end -local function get_encoding_variant() - return GUI.optionwidgets.encoding_variant_combo.selected -end - local function assert_settings_correct(encoding_variant) local errors = {} local settings = { @@ -210,8 +214,8 @@ local function get_stacks(images, encoding_variant, selection_type) end local tags = nil - -- Group images into sdr, extra pairs based on their original filename, ignoring the extension - -- Assume that the first encountered image from each filename is an sdr one, unless it has a tag matching the expected extra_image_type, or has the expected extension + -- Group images into (sdr [,extra]) stacks + -- Assume that the first encountered image from each stack is an sdr one, unless it has a tag matching the expected extra_image_type, or has the expected extension for k, v in pairs(images) do local is_extra = false tags = dt.tags.get_tags(v) @@ -224,7 +228,12 @@ local function get_stacks(images, encoding_variant, selection_type) is_extra = true end -- We assume every image in the stack is generated from the same source image file - local key = df.chop_filetype(v.path .. PS .. v.filename) + local key + if selection_type == SELECTION_TYPE_GROUP_BY_FNAME then + key = df.chop_filetype(v.path .. PS .. v.filename) + elseif selection_type == SELECTION_TYPE_ONE_STACK then + key = "the_one_and_only" + end if stacks[key] == nil then stacks[key] = {} end @@ -407,8 +416,9 @@ local function main() save_preferences() - local encoding_variant = get_encoding_variant() - log.msg(log.info, string.format("using encoding variant %d", encoding_variant)) + local selection_type = GUI.optionwidgets.selection_type_combo.selected + local encoding_variant = GUI.optionwidgets.encoding_variant_combo.selected + log.msg(log.info, string.format("using selection type %d, encoding variant %d", selection_type, encoding_variant)) local settings, errors = assert_settings_correct(encoding_variant) if not settings then @@ -417,7 +427,7 @@ local function main() return end - local stacks, stack_count = get_stacks(dt.gui.selection(), encoding_variant) + local stacks, stack_count = get_stacks(dt.gui.selection(), encoding_variant, selection_type) dt.print(string.format(_("Detected %d image stack(s)"), stack_count)) if stack_count == 0 then log.log_level(saved_log_level) @@ -513,20 +523,17 @@ GUI.optionwidgets.metadata_path_box = dt.new_widget("box") { } GUI.optionwidgets.encoding_variant_combo = dt.new_widget("combobox") { - label = _("Source images"), - tooltip = string.format(_([[Select types of images in the selection. + label = _("Each stack contains"), + tooltip = string.format(_([[Select the types of images in each stack. +This will determine the method used to generate UltraHDR. -- %s: SDR image paired with a gain map image -- %s: SDR image paired with a JPEG-XL HDR image (10-bit, 'PQ P3 RGB' profile recommended) +- %s: SDR image paired with a gain map image. +- %s: SDR image paired with a JPEG-XL HDR image (10-bit, 'PQ P3 RGB' profile recommended). - %s: SDR images only. Gainmaps will be copies of SDR images (the simplest option). -UltraHDR image will be created for each pair of images that: - - have the same underlying image path + filename (ignoring file extension) - - have the same dimensions - -By default, the first image in a pair is treated as SDR, and the second one stores extra gainmap/HDR data. -You can force the image into a specific slot by attaching "hdr" / "gainmap" tags. -]]), _("SDR + gainmap"), _("SDR + JPEG-XL HDR"), _("SDR (auto gainmap)")), +By default, the first image in a stack is treated as SDR, and the second one is a gainmap/HDR. +You can force the image into a specific stack slot by attaching "hdr" / "gainmap" tags to them. +]]), _("SDR + gainmap"), _("SDR + JPEG-XL HDR"), _("SDR only")), selected = 0, changed_callback = function(self) GUI.run.sensitive = self.selected and self.selected > 0 @@ -538,11 +545,28 @@ You can force the image into a specific slot by attaching "hdr" / "gainmap" tags end, _("SDR + gainmap"), -- ENCODING_VARIANT_SDR_AND_GAINMAP _("SDR + JPEG-XL HDR"), -- ENCODING_VARIANT_SDR_AND_HDR - _("SDR (auto gainmap)") -- ENCODING_VARIANT_SDR_AUTO_GAINMAP + _("SDR only") -- ENCODING_VARIANT_SDR_AUTO_GAINMAP +} + +GUI.optionwidgets.selection_type_combo = dt.new_widget("combobox") { + label = _("Selection contains"), + tooltip = string.format(_([[Select types of images selected in Darktable. +This determines how the plugin groups images into separate stacks (each stack will produce a single UltraHDR image). + +- %s: All selected image(s) belong to one stack. There will be 1 output UltraHDR image. +- %s: Group images into stacks, using the source image path + filename (ignoring extension). + Use this method if the source images for a given stack are Darktable duplicates. + +As an added precaution, each image in a stack needs to have the same dimensions. +]]), _("one stack"), _("multiple stacks (use filename)")), + selected = 0, + _("one stack"), -- SELECTION_TYPE_ONE_STACK + _("multiple stacks (use filename)") -- SELECTION_TYPE_GROUP_BY_FNAME } GUI.optionwidgets.encoding_settings_box = dt.new_widget("box") { orientation = "vertical", + GUI.optionwidgets.selection_type_combo, GUI.optionwidgets.encoding_variant_combo, GUI.optionwidgets.metadata_path_box } From 118bbad5cbdb6970d58417f1b6c9cdd60a55c1af Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Sun, 22 Sep 2024 17:02:24 +0200 Subject: [PATCH 22/41] Exposed sliders for crucial settings in a metadata file, removed file picker for metadata.cfg. Temporary metadata file will be created during UltraHDR generation, using the values from the UI. --- contrib/ultrahdr.lua | 173 +++++++++++++++++++++++++++++-------------- 1 file changed, 118 insertions(+), 55 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index c4fadde..4ca4b2a 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -68,11 +68,12 @@ local GUI = { output_directory_widget = {}, copy_exif = {}, import_to_darktable = {}, - metadata_file_generate = {}, - metadata_path_label = {}, - metadata_path_widget = {}, - metadata_path_box = {}, - metadata_path_box2 = {}, + min_content_boost = {}, + max_content_boost = {}, + hdr_capacity_min = {}, + hdr_capacity_max = {}, + metadata_label = {}, + metadata_box = {}, edit_executables_button = {}, executable_path_widget = {} }, @@ -100,22 +101,24 @@ local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3 local SELECTION_TYPE_ONE_STACK = 1 local SELECTION_TYPE_GROUP_BY_FNAME = 2 -local function generate_metadata_file() - local default_metadata_file = [[--maxContentBoost 6.0 ---minContentBoost 1.0 +local function generate_metadata_file(settings) + local metadata_file_fmt = [[--maxContentBoost %f +--minContentBoost %f --gamma 1.0 --offsetSdr 0.0 --offsetHdr 0.0 ---hdrCapacityMin 1.0 ---hdrCapacityMax 6.0]] +--hdrCapacityMin %f +--hdrCapacityMax %f]] - local filename = dt.configuration.config_dir .. PS .. "ultrahdr_metadata.cfg" + local filename = df.create_unique_filename(settings.tmpdir .. PS .. "metadata.cfg") local f, err = io.open(filename, "w+") if not f then dt.print(err) return nil end - f:write(default_metadata_file) + local content = string.format(metadata_file_fmt, settings.metadata.max_content_boost, + settings.metadata.min_content_boost, settings.metadata.hdr_capacity_min, settings.metadata.hdr_capacity_max) + f:write(content) f:close() return filename end @@ -123,15 +126,25 @@ end local function save_preferences() dt.preferences.write(namespace, "encoding_variant", "integer", GUI.optionwidgets.encoding_variant_combo.selected) dt.preferences.write(namespace, "selection_type", "integer", GUI.optionwidgets.selection_type_combo.selected) - if GUI.optionwidgets.metadata_path_widget.value then - dt.preferences.write(namespace, "metadata_path", "string", GUI.optionwidgets.metadata_path_widget.value) - end dt.preferences.write(namespace, "use_original_directory", "bool", GUI.optionwidgets.use_original_directory.value) if GUI.optionwidgets.output_directory_widget.value then dt.preferences.write(namespace, "output_directory", "string", GUI.optionwidgets.output_directory_widget.value) end dt.preferences.write(namespace, "import_to_darktable", "bool", GUI.optionwidgets.import_to_darktable.value) dt.preferences.write(namespace, "copy_exif", "bool", GUI.optionwidgets.copy_exif.value) + if GUI.optionwidgets.min_content_boost.value then + dt.preferences.write(namespace, "min_content_boost", "float", GUI.optionwidgets.min_content_boost.value) + dt.preferences.write(namespace, "max_content_boost", "float", GUI.optionwidgets.max_content_boost.value) + dt.preferences.write(namespace, "hdr_capacity_min", "float", GUI.optionwidgets.hdr_capacity_min.value) + dt.preferences.write(namespace, "hdr_capacity_max", "float", GUI.optionwidgets.hdr_capacity_max.value) + end +end + +local function default_to(value, default) + if value == 0 then + return default + end + return value end local function load_preferences() @@ -141,12 +154,6 @@ local function load_preferences() GUI.optionwidgets.selection_type_combo.selected = math.max( dt.preferences.read(namespace, "selection_type", "integer"), SELECTION_TYPE_ONE_STACK) - GUI.optionwidgets.metadata_path_widget.value = dt.preferences.read(namespace, "metadata_path", "string") - if not GUI.optionwidgets.metadata_path_widget.value then -- file widgets reset "" value to nil - GUI.optionwidgets.metadata_path_widget.value = generate_metadata_file() - -- Avoid regenerating the file if the user only enables the plugin, but never clicks "Generate" - dt.preferences.write(namespace, "metadata_path", "string", GUI.optionwidgets.metadata_path_widget.value) - end GUI.optionwidgets.output_directory_widget.value = dt.preferences.read(namespace, "output_directory", "string") GUI.optionwidgets.use_original_directory.value = dt.preferences.read(namespace, "use_original_directory", "bool") if not GUI.optionwidgets.output_directory_widget.value then @@ -154,6 +161,14 @@ local function load_preferences() end GUI.optionwidgets.import_to_darktable.value = dt.preferences.read(namespace, "import_to_darktable", "bool") GUI.optionwidgets.copy_exif.value = dt.preferences.read(namespace, "copy_exif", "bool") + GUI.optionwidgets.min_content_boost.value = default_to(dt.preferences.read(namespace, "min_content_boost", "float"), + 1.0) + GUI.optionwidgets.max_content_boost.value = default_to(dt.preferences.read(namespace, "max_content_boost", "float"), + 6.0) + GUI.optionwidgets.hdr_capacity_min.value = default_to(dt.preferences.read(namespace, "hdr_capacity_min", "float"), + 1.0) + GUI.optionwidgets.hdr_capacity_max.value = default_to(dt.preferences.read(namespace, "hdr_capacity_max", "float"), + 6.0) end local function assert_settings_correct(encoding_variant) @@ -168,7 +183,12 @@ local function assert_settings_correct(encoding_variant) use_original_dir = GUI.optionwidgets.use_original_directory.value, import_to_darktable = GUI.optionwidgets.import_to_darktable.value, copy_exif = GUI.optionwidgets.copy_exif.value, - metadata = GUI.optionwidgets.metadata_path_widget.value, + metadata = { + min_content_boost = GUI.optionwidgets.min_content_boost.value, + max_content_boost = GUI.optionwidgets.max_content_boost.value, + hdr_capacity_min = GUI.optionwidgets.hdr_capacity_min.value, + hdr_capacity_max = GUI.optionwidgets.hdr_capacity_max.value + }, tmpdir = dt.configuration.tmp_dir } @@ -183,8 +203,11 @@ local function assert_settings_correct(encoding_variant) end if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then - if not settings.metadata or not df.check_if_file_exists(settings.metadata) then - table.insert(errors, _("metadata.cfg file not found (use 'Generate' button)")) + if settings.metadata.min_content_boost >= settings.metadata.max_content_boost then + table.insert(errors, _("min_content_boost should not be greater than max_content_boost")) + end + if settings.metadata.hdr_capacity_min >= settings.metadata.hdr_capacity_max then + table.insert(errors, _("hdr_capacity_min should not be greater than hdr_capacity_max")) end end @@ -233,7 +256,7 @@ local function get_stacks(images, encoding_variant, selection_type) key = df.chop_filetype(v.path .. PS .. v.filename) elseif selection_type == SELECTION_TYPE_ONE_STACK then key = "the_one_and_only" - end + end if stacks[key] == nil then stacks[key] = {} end @@ -299,11 +322,11 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total function copy_or_export_jpg(src, dest) if df.get_filetype(src.filename) == "jpg" and not src.is_altered then df.file_copy(src.path .. PS .. src.filename, dest) - else + else local exporter = dt.new_format("jpeg") - exporter.quality = 95 + exporter.quality = 95 exporter:write_image(src, dest) - end + end end if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then @@ -328,11 +351,13 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total execute_cmd(settings.bin.exiftool .. " -all= " .. df.sanitize_filename(gainmap) .. " -overwrite_original") end update_job_progress() + -- Generate metadata.cfg file + local metadata_file = generate_metadata_file(settings) -- Merge files uhdr = df.chop_filetype(sdr) .. "_ultrahdr.jpg" execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -i " .. df.sanitize_filename(sdr .. ".noexif") .. " -g " .. - df.sanitize_filename(gainmap) .. " -f " .. df.sanitize_filename(settings.metadata) .. " -z " .. + df.sanitize_filename(gainmap) .. " -f " .. df.sanitize_filename(metadata_file) .. " -z " .. df.sanitize_filename(uhdr)) update_job_progress() -- Copy SDR's EXIF to UltraHDR file @@ -346,6 +371,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total -- Cleanup os.remove(sdr) os.remove(sdr .. ".noexif") + os.remove(metadata_file) if sdr ~= gainmap then os.remove(gainmap) end @@ -374,8 +400,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total local sdr_w, sdr_h = get_dimensions(images["sdr"]) execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr .. ".raw") .. " -p " .. df.sanitize_filename(extra) .. " -a 0 -b 3 -c 1 -C 1 -t 2 -M 1 -s 1 -q 95 -Q 95 -D 1 " .. " -w " .. - tostring(sdr_w) .. " -h " .. tostring(sdr_h) .. " -z " .. - df.sanitize_filename(uhdr)) + tostring(sdr_w) .. " -h " .. tostring(sdr_h) .. " -z " .. df.sanitize_filename(uhdr)) update_job_progress() if settings.copy_exif then -- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all). @@ -492,34 +517,72 @@ GUI.optionwidgets.output_settings_box = dt.new_widget("box") { GUI.optionwidgets.copy_exif } -GUI.optionwidgets.metadata_path_label = dt.new_widget("label") { - label = _("Gainmap metadata file") +GUI.optionwidgets.metadata_label = dt.new_widget("label") { + label = _("Gainmap metadata") } -GUI.optionwidgets.metadata_path_widget = dt.new_widget("file_chooser_button") { - title = _("Select libultrahdr metadata.cfg file"), - is_directory = false +GUI.optionwidgets.min_content_boost = dt.new_widget("slider") { + label = _('Min content boost'), + tooltip = _( + 'How much darker an image can get, when shown on an HDR display, relative to the SDR rendition (linear, SDR = 1.0). Also called "GainMapMin". '), + hard_min = 0.9, + hard_max = 10, + soft_min = 0.9, + soft_max = 2, + step = 1, + digits = 1, + reset_callback = function(self) + self.value = 1.0 + end } - -GUI.optionwidgets.metadata_file_generate = dt.new_widget("button") { - label = _("Generate"), - tooltip = _("Generate new metadata file with default values"), - clicked_callback = function() - local metadata_file = generate_metadata_file() - GUI.optionwidgets.metadata_path_widget.value = metadata_file +GUI.optionwidgets.max_content_boost = dt.new_widget("slider") { + label = _('Max content boost'), + tooltip = _( + 'How much brighter an image can get, when shown on an HDR display, relative to the SDR rendition (linear, SDR = 1.0). Also called "GainMapMax". \n\nMust not be lower than Min content boost'), + hard_min = 1, + hard_max = 10, + soft_min = 2, + soft_max = 10, + step = 1, + digits = 1, + reset_callback = function(self) + self.value = 6.0 + end +} +GUI.optionwidgets.hdr_capacity_min = dt.new_widget("slider") { + label = _('Min HDR capacity'), + tooltip = _('Minimum display boost value for which the gain map is applied at all (linear, SDR = 1.0).'), + hard_min = 0.9, + hard_max = 10, + soft_min = 1, + soft_max = 2, + step = 1, + digits = 1, + reset_callback = function(self) + self.value = 1.0 + end +} +GUI.optionwidgets.hdr_capacity_max = dt.new_widget("slider") { + label = _('Max HDR capacity'), + tooltip = _('Maximum display boost value for which the gain map is applied completely (linear, SDR = 1.0).'), + hard_min = 1, + hard_max = 10, + soft_min = 2, + soft_max = 10, + digits = 1, + step = 1, + reset_callback = function(self) + self.value = 6.0 end } -GUI.optionwidgets.metadata_path_box2 = dt.new_widget("box") { - orientation = "horizontal", - GUI.optionwidgets.metadata_path_widget, - GUI.optionwidgets.metadata_file_generate -} - -GUI.optionwidgets.metadata_path_box = dt.new_widget("box") { +GUI.optionwidgets.metadata_box = dt.new_widget("box") { orientation = "vertical", - GUI.optionwidgets.metadata_path_label, - GUI.optionwidgets.metadata_path_box2 + GUI.optionwidgets.metadata_label, + GUI.optionwidgets.min_content_boost, + GUI.optionwidgets.max_content_boost, + GUI.optionwidgets.hdr_capacity_min, + GUI.optionwidgets.hdr_capacity_max } GUI.optionwidgets.encoding_variant_combo = dt.new_widget("combobox") { @@ -538,9 +601,9 @@ You can force the image into a specific stack slot by attaching "hdr" / "gainmap changed_callback = function(self) GUI.run.sensitive = self.selected and self.selected > 0 if self.selected == ENCODING_VARIANT_SDR_AND_GAINMAP or self.selected == ENCODING_VARIANT_SDR_AUTO_GAINMAP then - GUI.optionwidgets.metadata_path_box.visible = true + GUI.optionwidgets.metadata_box.visible = true else - GUI.optionwidgets.metadata_path_box.visible = false + GUI.optionwidgets.metadata_box.visible = false end end, _("SDR + gainmap"), -- ENCODING_VARIANT_SDR_AND_GAINMAP @@ -568,7 +631,7 @@ GUI.optionwidgets.encoding_settings_box = dt.new_widget("box") { orientation = "vertical", GUI.optionwidgets.selection_type_combo, GUI.optionwidgets.encoding_variant_combo, - GUI.optionwidgets.metadata_path_box + GUI.optionwidgets.metadata_box } GUI.optionwidgets.executable_path_widget = df.executable_path_widget({"ultrahdr_app", "exiftool", "ffmpeg"}) From f6a1d1c20b7e162030fa75a00add12661e072e67 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Tue, 24 Sep 2024 14:57:35 +0200 Subject: [PATCH 23/41] Made the UltraHDR generation more robust. - Appropriate color profiles are used when generating UltraHDR files. DT_COLORSPACE_DISPLAY_P3 is used for SDR, and DT_COLORSPACE_PQ_P3 is used for HDR, regardless of what was selected in the export module UI. - Added correction for odd image dimensions in SDR + HDR mode. - Added quality setting for JPEG compression - SDR + HDR option can source from any DT image (JPEG-XL is generated on the fly). - generation can abort on errors and display error messages. --- contrib/ultrahdr.lua | 192 ++++++++++++++++++++++++++++++++----------- 1 file changed, 143 insertions(+), 49 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 4ca4b2a..f013ff3 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -75,7 +75,8 @@ local GUI = { metadata_label = {}, metadata_box = {}, edit_executables_button = {}, - executable_path_widget = {} + executable_path_widget = {}, + quality_widget = {} }, options = {}, run = {} @@ -101,6 +102,10 @@ local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3 local SELECTION_TYPE_ONE_STACK = 1 local SELECTION_TYPE_GROUP_BY_FNAME = 2 +-- Values are defined in darktable/src/common/colorspaces.h +local DT_COLORSPACE_PQ_P3 = 24 +local DT_COLORSPACE_DISPLAY_P3 = 26 + local function generate_metadata_file(settings) local metadata_file_fmt = [[--maxContentBoost %f --minContentBoost %f @@ -138,6 +143,7 @@ local function save_preferences() dt.preferences.write(namespace, "hdr_capacity_min", "float", GUI.optionwidgets.hdr_capacity_min.value) dt.preferences.write(namespace, "hdr_capacity_max", "float", GUI.optionwidgets.hdr_capacity_max.value) end + dt.preferences.write(namespace, "quality", "integer", GUI.optionwidgets.quality_widget.value) end local function default_to(value, default) @@ -169,6 +175,30 @@ local function load_preferences() 1.0) GUI.optionwidgets.hdr_capacity_max.value = default_to(dt.preferences.read(namespace, "hdr_capacity_max", "float"), 6.0) + GUI.optionwidgets.quality_widget.value = default_to(dt.preferences.read(namespace, "quality", "integer"), 95) +end + +-- Changes the combobox selection blindly until a paired config value is set. +-- Workaround for https://github.com/darktable-org/lua-scripts/issues/522 +local function set_combobox(path, instance, config_name, new_config_value) + + local pref = dt.preferences.read("darktable", config_name, "integer") + if pref == new_config_value then + return new_config_value + end + + dt.gui.action(path, 0, "selection", "first", 1.0) + local limit, i = 30, 0 -- in case there is no matching config value in the first n entries of a combobox. + while i < limit do + i = i + 1 + dt.gui.action(path, 0, "selection", "next", 1.0) + dt.control.sleep(10) + if dt.preferences.read("darktable", config_name, "integer") == new_config_value then + log.msg(log.debug, string.format(_("Changed %s from %d to %d"), config_name, pref, new_config_value)) + return pref + end + end + log.msg(log.error, string.format(_("Could not change %s from %d to %d"), config_name, pref, new_config_value)) end local function assert_settings_correct(encoding_variant) @@ -189,6 +219,7 @@ local function assert_settings_correct(encoding_variant) hdr_capacity_min = GUI.optionwidgets.hdr_capacity_min.value, hdr_capacity_max = GUI.optionwidgets.hdr_capacity_max.value }, + quality = GUI.optionwidgets.quality_widget.value, tmpdir = dt.configuration.tmp_dir } @@ -226,11 +257,10 @@ end local function get_stacks(images, encoding_variant, selection_type) local stacks = {} - local extra_image_content_type, extra_image_extension + local extra_image_content_type if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then extra_image_content_type = "gainmap" elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then - extra_image_extension = "jxl" extra_image_content_type = "hdr" elseif encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then extra_image_content_type = nil @@ -278,9 +308,6 @@ local function get_stacks(images, encoding_variant, selection_type) if extra_image_content_type then if not v["sdr"] or not v[extra_image_content_type] then stacks[k] = nil - elseif extra_image_extension and df.get_filetype(v[extra_image_content_type].filename) ~= - extra_image_extension then - stacks[k] = nil else local sdr_w, sdr_h = get_dimensions(v["sdr"]) local extra_w, extra_h = get_dimensions(v[extra_image_content_type]) @@ -309,6 +336,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total local total_substeps local substep = 0 local uhdr + local errors = {} function update_job_progress() substep = substep + 1 @@ -319,28 +347,56 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total job.percent = (total_substeps * step + substep) / (total_steps * total_substeps) end - function copy_or_export_jpg(src, dest) - if df.get_filetype(src.filename) == "jpg" and not src.is_altered then - df.file_copy(src.path .. PS .. src.filename, dest) + function copy_or_export(src_image, dest, format, colorspace, props) + if df.get_filetype(src_image.filename) == df.get_filetype(dest) and not src_image.is_altered then + return df.file_copy(src_image.path .. PS .. src_image.filename, dest) else - local exporter = dt.new_format("jpeg") - exporter.quality = 95 - exporter:write_image(src, dest) + local prev = set_combobox("lib/export/profile", 0, "plugins/lighttable/export/icctype", colorspace) + if not prev then + return false + end + local exporter = dt.new_format(format) + for k, v in pairs(props) do + exporter[k] = v + end + local ok = not exporter:write_image(src_image, dest) + if prev then + set_combobox("lib/export/profile", 0, "plugins/lighttable/export/icctype", prev) + end + return ok end + return true end if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then total_substeps = 6 + local ok -- Export/copy both SDR and gainmap to JPEGs local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) .. ".jpg") - copy_or_export_jpg(images["sdr"], sdr) + ok = copy_or_export(images["sdr"], sdr, "jpeg", DT_COLORSPACE_DISPLAY_P3, { + quality = settings.quality + }) + if not ok then + os.remove(sdr) + table.insert(errors, string.format(_("Error exporting %s to %s"), images["sdr"].filename, "jpeg")) + return false, errors + end + local gainmap if encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then -- SDR is also a gainmap gainmap = sdr else gainmap = df.create_unique_filename(settings.tmpdir .. PS .. images["gainmap"].filename .. "_gainmap.jpg") - copy_or_export_jpg(images["gainmap"], gainmap) + ok = copy_or_export(images["gainmap"], gainmap, "jpeg", DT_COLORSPACE_DISPLAY_P3, { + quality = settings.quality + }) + if not ok then + os.remove(sdr) + os.remove(sdr) + table.insert(errors, string.format(_("Error exporting %s to %s"), images["gainmap"].filename, "jpeg")) + return false, errors + end end log.msg(log.debug, string.format(_("Exported files: %s, %s"), sdr, gainmap)) update_job_progress() @@ -377,30 +433,54 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total end update_job_progress() elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then - total_substeps = 5 + local ok + total_substeps = 6 -- https://discuss.pixls.us/t/manual-creation-of-ultrahdr-images/45004/20 - -- Step 1: Export SDR to PNG (HDR is already a JPEG-XL) - local exporter = dt.new_format("png") - exporter.bpp = 8 + -- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3 + local hdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) .. + ".jxl") + ok = copy_or_export(images["hdr"], hdr, "jpegxl", DT_COLORSPACE_PQ_P3, { + bpp = 10, + quality = 100 -- lossless + }) + if not ok then + os.remove(hdr) + table.insert(errors, string.format(_("Error exporting %s to %s"), images["hdr"].filename, "jxl")) + return false, errors + end + update_job_progress() + -- Step 2: Export SDR to PNG local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) .. ".png") - exporter:write_image(images["sdr"], sdr) + ok = copy_or_export(images["sdr"], sdr, "png", DT_COLORSPACE_DISPLAY_P3, { + bpp = 8 + }) + if not ok then + os.remove(hdr) + os.remove(sdr) + table.insert(errors, string.format(_("Error exporting %s to %s"), images["sdr"].filename, "png")) + return false, errors + end uhdr = df.chop_filetype(sdr) .. "_ultrahdr.jpg" update_job_progress() - local extra = df.create_unique_filename(settings.tmpdir .. PS .. images["hdr"].filename .. ".raw") - - -- Step 3: Generate libultrahdr RAW images - execute_cmd(settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(sdr) .. " -pix_fmt rgba -f rawvideo " .. - df.sanitize_filename(sdr .. ".raw")) - execute_cmd(settings.bin.ffmpeg .. " -i " .. - df.sanitize_filename(images["hdr"].path .. PS .. images["hdr"].filename) .. - " -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(extra)) - update_job_progress() + -- Step 3: Generate libultrahdr RAW images local sdr_w, sdr_h = get_dimensions(images["sdr"]) + local resize_cmd = "" + if sdr_h % 2 + sdr_w % 2 > 0 then -- needs resizing to even dimensions. + resize_cmd = string.format(" -vf 'crop=%d:%d:0:0' ", sdr_w - sdr_w % 2, sdr_h - sdr_h % 2) + end + + execute_cmd(settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(sdr) .. resize_cmd .. " -pix_fmt rgba -f rawvideo " .. + df.sanitize_filename(sdr .. ".raw")) + execute_cmd(settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(hdr) .. resize_cmd .. " -pix_fmt p010le -f rawvideo " .. + df.sanitize_filename(hdr .. ".raw")) + update_job_progress() execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr .. ".raw") .. " -p " .. - df.sanitize_filename(extra) .. " -a 0 -b 3 -c 1 -C 1 -t 2 -M 1 -s 1 -q 95 -Q 95 -D 1 " .. " -w " .. - tostring(sdr_w) .. " -h " .. tostring(sdr_h) .. " -z " .. df.sanitize_filename(uhdr)) + df.sanitize_filename(hdr .. ".raw") .. + string.format(" -a 0 -b 3 -c 1 -C 1 -t 2 -M 1 -s 1 -q %d -Q %d -D 1 ", settings.quality, + settings.quality) .. " -w " .. tostring(sdr_w - sdr_w % 2) .. " -h " .. tostring(sdr_h - sdr_h % 2) .. " -z " .. + df.sanitize_filename(uhdr)) update_job_progress() if settings.copy_exif then -- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all). @@ -409,9 +489,10 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total df.sanitize_filename(uhdr) .. " -overwrite_original -preserve") end -- Cleanup + os.remove(hdr) os.remove(sdr) + os.remove(hdr .. ".raw") os.remove(sdr .. ".raw") - os.remove(extra) update_job_progress() end @@ -433,12 +514,10 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total log.msg(log.info, msg) dt.print(msg) update_job_progress() + return true, nil end local function main() - local saved_log_level = log.log_level() - log.log_level(log.info) - save_preferences() local selection_type = GUI.optionwidgets.selection_type_combo.selected @@ -447,21 +526,25 @@ local function main() local settings, errors = assert_settings_correct(encoding_variant) if not settings then - dt.print(string.format(_("Export settings are incorrect, exiting:\n\n%s"), table.concat(errors, "\n"))) - log.log_level(saved_log_level) + dt.print(string.format(_("Export settings are incorrect, exiting:\n\n- %s"), table.concat(errors, "\n- "))) return end local stacks, stack_count = get_stacks(dt.gui.selection(), encoding_variant, selection_type) dt.print(string.format(_("Detected %d image stack(s)"), stack_count)) if stack_count == 0 then - log.log_level(saved_log_level) return end job = dt.gui.create_job(_("Generating UltraHDR images"), true, stop_job) local count = 0 + local msg for i, v in pairs(stacks) do - generate_ultrahdr(encoding_variant, v, settings, count, stack_count) + local ok, errors = generate_ultrahdr(encoding_variant, v, settings, count, stack_count) + if not ok then + dt.print(string.format(_("Errors generating images:\n\n- %s"), table.concat(errors, "\n- "))) + job.valid = false + return + end count = count + 1 -- sleep for a short moment to give stop_job callback function a chance to run dt.control.sleep(10) @@ -471,10 +554,9 @@ local function main() job.valid = false end - local msg = string.format(_("Generated %d UltraHDR image(s)."), count) + msg = string.format(_("Generated %d UltraHDR image(s)."), count) log.msg(log.info, msg) dt.print(msg) - log.log_level(saved_log_level) end GUI.optionwidgets.settings_label = dt.new_widget("section_label") { @@ -591,12 +673,12 @@ GUI.optionwidgets.encoding_variant_combo = dt.new_widget("combobox") { This will determine the method used to generate UltraHDR. - %s: SDR image paired with a gain map image. -- %s: SDR image paired with a JPEG-XL HDR image (10-bit, 'PQ P3 RGB' profile recommended). -- %s: SDR images only. Gainmaps will be copies of SDR images (the simplest option). +- %s: SDR image paired with an HDR image. +- %s: Each stack consists of a single SDR image. Gainmaps will be copies of SDR images. By default, the first image in a stack is treated as SDR, and the second one is a gainmap/HDR. -You can force the image into a specific stack slot by attaching "hdr" / "gainmap" tags to them. -]]), _("SDR + gainmap"), _("SDR + JPEG-XL HDR"), _("SDR only")), +You can force the image into a specific stack slot by attaching "hdr" / "gainmap" tags to it. +]]), _("SDR + gainmap"), _("SDR + HDR"), _("SDR only")), selected = 0, changed_callback = function(self) GUI.run.sensitive = self.selected and self.selected > 0 @@ -607,7 +689,7 @@ You can force the image into a specific stack slot by attaching "hdr" / "gainmap end end, _("SDR + gainmap"), -- ENCODING_VARIANT_SDR_AND_GAINMAP - _("SDR + JPEG-XL HDR"), -- ENCODING_VARIANT_SDR_AND_HDR + _("SDR + HDR"), -- ENCODING_VARIANT_SDR_AND_HDR _("SDR only") -- ENCODING_VARIANT_SDR_AUTO_GAINMAP } @@ -627,10 +709,25 @@ As an added precaution, each image in a stack needs to have the same dimensions. _("multiple stacks (use filename)") -- SELECTION_TYPE_GROUP_BY_FNAME } +GUI.optionwidgets.quality_widget = dt.new_widget("slider") { + label = _('Quality'), + tooltip = _('Quality of the output UltraHDR JPEG file'), + hard_min = 0, + hard_max = 100, + soft_min = 0, + soft_max = 100, + step = 1, + digits = 0, + reset_callback = function(self) + self.value = 95 + end +} + GUI.optionwidgets.encoding_settings_box = dt.new_widget("box") { orientation = "vertical", GUI.optionwidgets.selection_type_combo, GUI.optionwidgets.encoding_variant_combo, + GUI.optionwidgets.quality_widget, GUI.optionwidgets.metadata_box } @@ -656,10 +753,7 @@ GUI.options = dt.new_widget("box") { GUI.run = dt.new_widget("button") { label = _("Generate UltraHDR"), - tooltip = _([[Generate UltraHDR image(s) from selection - -Global options in the export module apply to the SDR image. Make sure that a proper color 'profile' setting is used (e.g. Display P3) -]]), + tooltip = _("Generate UltraHDR image(s) from selection"), clicked_callback = main } From 3791b47914bce99129fa3eed69f06d20772b5deb Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Tue, 24 Sep 2024 15:03:52 +0200 Subject: [PATCH 24/41] Use lowercase labels to adjust to DT style. --- contrib/ultrahdr.lua | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index f013ff3..4d37a6c 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -1,6 +1,6 @@ --[[ - UltraHDR storage for darktable + UltraHDR image generation for darktable copyright (c) 2024 Krzysztof Kotowicz @@ -20,7 +20,7 @@ ]] --[[ ULTRAHDR -Generate UltraHDR JPG images from various combinations of source files (SDR, HDR, gainmap). +Generate UltraHDR JPEG images from various combinations of source files (SDR, HDR, gainmap). https://developer.android.com/media/platform/hdr-image-format @@ -34,6 +34,7 @@ ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT USAGE * require this file from your main luarc config file * set binary tool paths +* Use UltraHDR module to generate UltraHDR images from selection ]] local dt = require "darktable" local du = require "lib/dtutils" @@ -564,16 +565,16 @@ GUI.optionwidgets.settings_label = dt.new_widget("section_label") { } GUI.optionwidgets.output_settings_label = dt.new_widget("section_label") { - label = _("Output") + label = _("output") } GUI.optionwidgets.output_directory_widget = dt.new_widget("file_chooser_button") { - title = _("Select directory to write UltraHDR image files to"), + title = _("select directory to write UltraHDR image files to"), is_directory = true } GUI.optionwidgets.use_original_directory = dt.new_widget("check_button") { - label = _("Export to original directory"), + label = _("export to original directory"), tooltip = _("Write UltraHDR images to the same directory as their original images"), clicked_callback = function(self) GUI.optionwidgets.output_directory_widget.sensitive = not self.value @@ -581,12 +582,12 @@ GUI.optionwidgets.use_original_directory = dt.new_widget("check_button") { } GUI.optionwidgets.import_to_darktable = dt.new_widget("check_button") { - label = _("Import UltraHDRs to Darktable"), + label = _("import UltraHDRs to library"), tooltip = _("Import UltraHDR images to Darktable library after generating, with an 'ultrahdr' tag attached.") } GUI.optionwidgets.copy_exif = dt.new_widget("check_button") { - label = _("Copy EXIF data from SDR file(s)"), + label = _("copy EXIF data"), tooltip = _("Copy EXIF data into UltraHDR file(s) from their SDR sources.") } @@ -600,11 +601,11 @@ GUI.optionwidgets.output_settings_box = dt.new_widget("box") { } GUI.optionwidgets.metadata_label = dt.new_widget("label") { - label = _("Gainmap metadata") + label = _("gainmap metadata") } GUI.optionwidgets.min_content_boost = dt.new_widget("slider") { - label = _('Min content boost'), + label = _('min content boost'), tooltip = _( 'How much darker an image can get, when shown on an HDR display, relative to the SDR rendition (linear, SDR = 1.0). Also called "GainMapMin". '), hard_min = 0.9, @@ -618,7 +619,7 @@ GUI.optionwidgets.min_content_boost = dt.new_widget("slider") { end } GUI.optionwidgets.max_content_boost = dt.new_widget("slider") { - label = _('Max content boost'), + label = _('max content boost'), tooltip = _( 'How much brighter an image can get, when shown on an HDR display, relative to the SDR rendition (linear, SDR = 1.0). Also called "GainMapMax". \n\nMust not be lower than Min content boost'), hard_min = 1, @@ -632,7 +633,7 @@ GUI.optionwidgets.max_content_boost = dt.new_widget("slider") { end } GUI.optionwidgets.hdr_capacity_min = dt.new_widget("slider") { - label = _('Min HDR capacity'), + label = _('min HDR capacity'), tooltip = _('Minimum display boost value for which the gain map is applied at all (linear, SDR = 1.0).'), hard_min = 0.9, hard_max = 10, @@ -645,7 +646,7 @@ GUI.optionwidgets.hdr_capacity_min = dt.new_widget("slider") { end } GUI.optionwidgets.hdr_capacity_max = dt.new_widget("slider") { - label = _('Max HDR capacity'), + label = _('max HDR capacity'), tooltip = _('Maximum display boost value for which the gain map is applied completely (linear, SDR = 1.0).'), hard_min = 1, hard_max = 10, @@ -668,7 +669,7 @@ GUI.optionwidgets.metadata_box = dt.new_widget("box") { } GUI.optionwidgets.encoding_variant_combo = dt.new_widget("combobox") { - label = _("Each stack contains"), + label = _("each stack contains"), tooltip = string.format(_([[Select the types of images in each stack. This will determine the method used to generate UltraHDR. @@ -694,7 +695,7 @@ You can force the image into a specific stack slot by attaching "hdr" / "gainmap } GUI.optionwidgets.selection_type_combo = dt.new_widget("combobox") { - label = _("Selection contains"), + label = _("selection contains"), tooltip = string.format(_([[Select types of images selected in Darktable. This determines how the plugin groups images into separate stacks (each stack will produce a single UltraHDR image). @@ -710,7 +711,7 @@ As an added precaution, each image in a stack needs to have the same dimensions. } GUI.optionwidgets.quality_widget = dt.new_widget("slider") { - label = _('Quality'), + label = _('quality'), tooltip = _('Quality of the output UltraHDR JPEG file'), hard_min = 0, hard_max = 100, @@ -735,7 +736,7 @@ GUI.optionwidgets.executable_path_widget = df.executable_path_widget({"ultrahdr_ GUI.optionwidgets.executable_path_widget.visible = false GUI.optionwidgets.edit_executables_button = dt.new_widget("button") { - label = _("Show / hide executables"), + label = _("show / hide executables"), tooltip = _("Show / hide settings for executable files required for the plugin functionality"), clicked_callback = function() GUI.optionwidgets.executable_path_widget.visible = not GUI.optionwidgets.executable_path_widget.visible @@ -752,7 +753,7 @@ GUI.options = dt.new_widget("box") { } GUI.run = dt.new_widget("button") { - label = _("Generate UltraHDR"), + label = _("generate UltraHDR"), tooltip = _("Generate UltraHDR image(s) from selection"), clicked_callback = main } From 84a1c1ddfe4ca43e919f95f11f40e58169f7727b Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Wed, 25 Sep 2024 19:47:18 +0200 Subject: [PATCH 25/41] Use single channel gainmaps in SDR+HDR. --- contrib/ultrahdr.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 4d37a6c..a3b30e0 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -479,7 +479,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total update_job_progress() execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr .. ".raw") .. " -p " .. df.sanitize_filename(hdr .. ".raw") .. - string.format(" -a 0 -b 3 -c 1 -C 1 -t 2 -M 1 -s 1 -q %d -Q %d -D 1 ", settings.quality, + string.format(" -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -s 1 -q %d -Q %d -D 1 ", settings.quality, settings.quality) .. " -w " .. tostring(sdr_w - sdr_w % 2) .. " -h " .. tostring(sdr_h - sdr_h % 2) .. " -z " .. df.sanitize_filename(uhdr)) update_job_progress() From 749db97dd53fbf21b98042b759de7c61df006c7d Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Wed, 25 Sep 2024 19:47:41 +0200 Subject: [PATCH 26/41] Speed up export in HDR+SDR. --- contrib/ultrahdr.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index a3b30e0..508ef8d 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -442,7 +442,8 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total ".jxl") ok = copy_or_export(images["hdr"], hdr, "jpegxl", DT_COLORSPACE_PQ_P3, { bpp = 10, - quality = 100 -- lossless + quality = 100, -- lossless + effort = 1, -- we don't care about the size, the faile is temporary. }) if not ok then os.remove(hdr) From 2a4452cd20fcb7586556d2e8ad61264674ab1074 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Wed, 25 Sep 2024 21:30:09 +0200 Subject: [PATCH 27/41] Workaround https://github.com/darktable-org/darktable/pull/17529 for future DT versions. --- contrib/ultrahdr.lua | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 508ef8d..f1f859c 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -46,8 +46,8 @@ local gettext = dt.gettext.gettext local namespace = "module_ultrahdr" --- works with darktable API version from 5.0.0 on -du.check_min_api_version("7.0.0", "ultrahdr") +-- works with darktable API version from 4.8.0 on +du.check_min_api_version("9.3.0", "ultrahdr") dt.gettext.bindtextdomain(namespace, dt.configuration.config_dir .. "/lua/locale/") @@ -360,7 +360,12 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total for k, v in pairs(props) do exporter[k] = v end - local ok = not exporter:write_image(src_image, dest) + local ok = exporter:write_image(src_image, dest) + if dt.configuration.api_version_string == "9.3.0" then + -- Workaround for https://github.com/darktable-org/darktable/issues/17528 + ok = not ok + end + if prev then set_combobox("lib/export/profile", 0, "plugins/lighttable/export/icctype", prev) end From ea7ac8e474bd8a066f2ee034f1401f9cd360606a Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Wed, 25 Sep 2024 21:38:35 +0200 Subject: [PATCH 28/41] Abort and cleanup early when any of the binaries fail. --- contrib/ultrahdr.lua | 142 ++++++++++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 57 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index f1f859c..83568c8 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -328,16 +328,23 @@ local function stop_job(job) job.valid = false end -local function execute_cmd(cmd) - log.msg(log.debug, cmd) - return dtsys.external_command(cmd) -end - local function generate_ultrahdr(encoding_variant, images, settings, step, total_steps) local total_substeps local substep = 0 local uhdr local errors = {} + local remove_files = {} + local ok + local cmd + + local function execute_cmd(cmd, errormsg) + log.msg(log.debug, cmd) + local code = dtsys.external_command(cmd) + if errormsg and code > 0 then + table.insert(errors, errormsg) + end + return code == 0 + end function update_job_progress() substep = substep + 1 @@ -374,19 +381,25 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total return true end + function cleanup() + for _, v in pairs(remove_files) do + os.remove(v) + end + return false + end + if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then - total_substeps = 6 - local ok + total_substeps = 5 -- Export/copy both SDR and gainmap to JPEGs local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) .. ".jpg") + table.insert(remove_files, sdr) ok = copy_or_export(images["sdr"], sdr, "jpeg", DT_COLORSPACE_DISPLAY_P3, { quality = settings.quality }) if not ok then - os.remove(sdr) table.insert(errors, string.format(_("Error exporting %s to %s"), images["sdr"].filename, "jpeg")) - return false, errors + return cleanup(), errors end local gainmap @@ -394,52 +407,56 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total gainmap = sdr else gainmap = df.create_unique_filename(settings.tmpdir .. PS .. images["gainmap"].filename .. "_gainmap.jpg") + table.insert(remove_files, gainmap) ok = copy_or_export(images["gainmap"], gainmap, "jpeg", DT_COLORSPACE_DISPLAY_P3, { quality = settings.quality }) if not ok then - os.remove(sdr) - os.remove(sdr) table.insert(errors, string.format(_("Error exporting %s to %s"), images["gainmap"].filename, "jpeg")) - return false, errors + return cleanup(), errors end end log.msg(log.debug, string.format(_("Exported files: %s, %s"), sdr, gainmap)) update_job_progress() -- Strip EXIFs - execute_cmd(settings.bin.exiftool .. " -all= " .. df.sanitize_filename(sdr) .. " -o " .. - df.sanitize_filename(sdr .. ".noexif")) + table.insert(remove_files, sdr .. ".noexif") + cmd = settings.bin.exiftool .. " -all= " .. df.sanitize_filename(sdr) .. " -o " .. + df.sanitize_filename(sdr .. ".noexif") + if not execute_cmd(cmd, string.format(_("Error stripping EXIF from %s"), sdr)) then + return cleanup(), errors + end if sdr ~= gainmap then - execute_cmd(settings.bin.exiftool .. " -all= " .. df.sanitize_filename(gainmap) .. " -overwrite_original") + if not execute_cmd(settings.bin.exiftool .. " -all= " .. df.sanitize_filename(gainmap) .. + " -overwrite_original", string.format(_("Error stripping EXIF from %s"), gainmap)) then + return cleanup(), errors + end end update_job_progress() -- Generate metadata.cfg file local metadata_file = generate_metadata_file(settings) + table.insert(remove_files, metadata_file) -- Merge files uhdr = df.chop_filetype(sdr) .. "_ultrahdr.jpg" - - execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -i " .. df.sanitize_filename(sdr .. ".noexif") .. " -g " .. - df.sanitize_filename(gainmap) .. " -f " .. df.sanitize_filename(metadata_file) .. " -z " .. - df.sanitize_filename(uhdr)) + table.insert(remove_files, uhdr) + cmd = settings.bin.ultrahdr_app .. " -m 0 -i " .. df.sanitize_filename(sdr .. ".noexif") .. " -g " .. + df.sanitize_filename(gainmap) .. " -f " .. df.sanitize_filename(metadata_file) .. " -z " .. + df.sanitize_filename(uhdr) + if not execute_cmd(cmd, string.format(_("Error merging UltraHDR to %s"), uhdr)) then + return cleanup(), errors + end update_job_progress() -- Copy SDR's EXIF to UltraHDR file if settings.copy_exif then -- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all). -- This might hapen e.g. when the source files are Adobe gainmap HDRs. - execute_cmd(settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -exif " .. - df.sanitize_filename(uhdr) .. " -overwrite_original -preserve") - end - update_job_progress() - -- Cleanup - os.remove(sdr) - os.remove(sdr .. ".noexif") - os.remove(metadata_file) - if sdr ~= gainmap then - os.remove(gainmap) + cmd = settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -exif " .. + df.sanitize_filename(uhdr) .. " -overwrite_original -preserve" + if not execute_cmd(cmd, string.format(_("Error adding EXIF to %s"), uhdr)) then + return cleanup(), errors + end end update_job_progress() elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then - local ok total_substeps = 6 -- https://discuss.pixls.us/t/manual-creation-of-ultrahdr-images/45004/20 -- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3 @@ -448,28 +465,26 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total ok = copy_or_export(images["hdr"], hdr, "jpegxl", DT_COLORSPACE_PQ_P3, { bpp = 10, quality = 100, -- lossless - effort = 1, -- we don't care about the size, the faile is temporary. + effort = 1 -- we don't care about the size, the faile is temporary. }) if not ok then - os.remove(hdr) table.insert(errors, string.format(_("Error exporting %s to %s"), images["hdr"].filename, "jxl")) - return false, errors + return cleanup(), errors end update_job_progress() -- Step 2: Export SDR to PNG local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) .. ".png") + table.insert(remove_files, sdr) ok = copy_or_export(images["sdr"], sdr, "png", DT_COLORSPACE_DISPLAY_P3, { bpp = 8 }) if not ok then - os.remove(hdr) - os.remove(sdr) table.insert(errors, string.format(_("Error exporting %s to %s"), images["sdr"].filename, "png")) - return false, errors + return cleanup(), errors end uhdr = df.chop_filetype(sdr) .. "_ultrahdr.jpg" - + table.insert(remove_files, uhdr) update_job_progress() -- Step 3: Generate libultrahdr RAW images local sdr_w, sdr_h = get_dimensions(images["sdr"]) @@ -477,35 +492,48 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total if sdr_h % 2 + sdr_w % 2 > 0 then -- needs resizing to even dimensions. resize_cmd = string.format(" -vf 'crop=%d:%d:0:0' ", sdr_w - sdr_w % 2, sdr_h - sdr_h % 2) end - - execute_cmd(settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(sdr) .. resize_cmd .. " -pix_fmt rgba -f rawvideo " .. - df.sanitize_filename(sdr .. ".raw")) - execute_cmd(settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(hdr) .. resize_cmd .. " -pix_fmt p010le -f rawvideo " .. - df.sanitize_filename(hdr .. ".raw")) + table.insert(remove_files, sdr .. ".raw") + table.insert(remove_files, hdr .. ".raw") + cmd = + settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(sdr) .. resize_cmd .. " -pix_fmt rgba -f rawvideo " .. + df.sanitize_filename(sdr .. ".raw") + if not execute_cmd(cmd, string.format(_("Error generating %s"), sdr .. ".raw")) then + return cleanup(), errors + end + cmd = settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(hdr) .. resize_cmd .. + " -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(hdr .. ".raw") + if not execute_cmd(cmd, string.format(_("Error generating %s"), hdr .. ".raw")) then + return cleanup(), errors + end update_job_progress() - execute_cmd(settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr .. ".raw") .. " -p " .. - df.sanitize_filename(hdr .. ".raw") .. - string.format(" -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -s 1 -q %d -Q %d -D 1 ", settings.quality, - settings.quality) .. " -w " .. tostring(sdr_w - sdr_w % 2) .. " -h " .. tostring(sdr_h - sdr_h % 2) .. " -z " .. - df.sanitize_filename(uhdr)) + cmd = settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr .. ".raw") .. " -p " .. + df.sanitize_filename(hdr .. ".raw") .. + string.format(" -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -s 1 -q %d -Q %d -D 1 ", settings.quality, + settings.quality) .. " -w " .. tostring(sdr_w - sdr_w % 2) .. " -h " .. tostring(sdr_h - sdr_h % 2) .. + " -z " .. df.sanitize_filename(uhdr) + if not execute_cmd(cmd, string.format(_("Error merging %s"), uhdr)) then + return cleanup(), errors + end update_job_progress() if settings.copy_exif then -- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all). -- This might hapen e.g. when the source files are Adobe gainmap HDRs. - execute_cmd(settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -exif " .. - df.sanitize_filename(uhdr) .. " -overwrite_original -preserve") + cmd = settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -exif " .. + df.sanitize_filename(uhdr) .. " -overwrite_original -preserve" + if not execute_cmd(cmd, string.format(_("Error adding EXIF to %s"), uhdr)) then + return cleanup(), errors + end end - -- Cleanup - os.remove(hdr) - os.remove(sdr) - os.remove(hdr .. ".raw") - os.remove(sdr .. ".raw") update_job_progress() end local output_dir = settings.use_original_dir and images["sdr"].path or settings.output local output_file = df.create_unique_filename(output_dir .. PS .. df.get_filename(uhdr)) - df.file_move(uhdr, output_file) + ok = df.file_move(uhdr, output_file) + if not ok then + table.insert(errors, string.format(_("Error generating UltraHDR for %s"), images["sdr"].filename)) + return cleanup(), errors + end if settings.import_to_darktable then local img = dt.database.import(output_file) -- Add "ultrahdr" tag to the imported image @@ -516,11 +544,11 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total end dt.tags.attach(tagnr, img) end - + cleanup() + update_job_progress() local msg = string.format(_("Generated %s."), df.get_filename(output_file)) log.msg(log.info, msg) dt.print(msg) - update_job_progress() return true, nil end From 268aa7df42dadb1770f078b2a7fffcb677b636e4 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Thu, 26 Sep 2024 09:05:31 +0200 Subject: [PATCH 29/41] Removed darktable capitalization. --- contrib/ultrahdr.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 83568c8..d26a72f 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -617,7 +617,7 @@ GUI.optionwidgets.use_original_directory = dt.new_widget("check_button") { GUI.optionwidgets.import_to_darktable = dt.new_widget("check_button") { label = _("import UltraHDRs to library"), - tooltip = _("Import UltraHDR images to Darktable library after generating, with an 'ultrahdr' tag attached.") + tooltip = _("Import UltraHDR images to darktable library after generating, with an 'ultrahdr' tag attached.") } GUI.optionwidgets.copy_exif = dt.new_widget("check_button") { @@ -730,12 +730,12 @@ You can force the image into a specific stack slot by attaching "hdr" / "gainmap GUI.optionwidgets.selection_type_combo = dt.new_widget("combobox") { label = _("selection contains"), - tooltip = string.format(_([[Select types of images selected in Darktable. + tooltip = string.format(_([[Select types of images selected in darktable. This determines how the plugin groups images into separate stacks (each stack will produce a single UltraHDR image). - %s: All selected image(s) belong to one stack. There will be 1 output UltraHDR image. - %s: Group images into stacks, using the source image path + filename (ignoring extension). - Use this method if the source images for a given stack are Darktable duplicates. + Use this method if the source images for a given stack are darktable duplicates. As an added precaution, each image in a stack needs to have the same dimensions. ]]), _("one stack"), _("multiple stacks (use filename)")), From 13ae486ab3abbebbb8c5f379748a9cf249573ca9 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Thu, 26 Sep 2024 10:02:47 +0200 Subject: [PATCH 30/41] Verify raw file size in pixels. Added option to keep temporary files around for analysis. --- contrib/ultrahdr.lua | 45 +++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index d26a72f..74baab9 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -221,7 +221,8 @@ local function assert_settings_correct(encoding_variant) hdr_capacity_max = GUI.optionwidgets.hdr_capacity_max.value }, quality = GUI.optionwidgets.quality_widget.value, - tmpdir = dt.configuration.tmp_dir + tmpdir = dt.configuration.tmp_dir, + skip_cleanup = false -- keep temporary files around, for debugging. } if not settings.use_original_dir and (not settings.output or not df.check_if_file_exists(settings.output)) then @@ -328,6 +329,16 @@ local function stop_job(job) job.valid = false end +local function file_size(path) + local f, err = io.open(path, "r") + if not f then + return 0 + end + local size = f:seek("end") + f:close() + return size +end + local function generate_ultrahdr(encoding_variant, images, settings, step, total_steps) local total_substeps local substep = 0 @@ -382,6 +393,9 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total end function cleanup() + if settings.skip_cleanup then + return false + end for _, v in pairs(remove_files) do os.remove(v) end @@ -462,6 +476,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total -- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3 local hdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) .. ".jxl") + table.insert(remove_files, hdr) ok = copy_or_export(images["hdr"], hdr, "jpegxl", DT_COLORSPACE_PQ_P3, { bpp = 10, quality = 100, -- lossless @@ -487,27 +502,34 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total table.insert(remove_files, uhdr) update_job_progress() -- Step 3: Generate libultrahdr RAW images + local sdr_raw, hdr_raw = sdr .. ".raw", hdr .. ".raw" + table.insert(remove_files, sdr_raw) + table.insert(remove_files, hdr_raw) local sdr_w, sdr_h = get_dimensions(images["sdr"]) local resize_cmd = "" if sdr_h % 2 + sdr_w % 2 > 0 then -- needs resizing to even dimensions. resize_cmd = string.format(" -vf 'crop=%d:%d:0:0' ", sdr_w - sdr_w % 2, sdr_h - sdr_h % 2) end - table.insert(remove_files, sdr .. ".raw") - table.insert(remove_files, hdr .. ".raw") + local size_in_px = (sdr_w - sdr_w % 2) * (sdr_h - sdr_h % 2) cmd = settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(sdr) .. resize_cmd .. " -pix_fmt rgba -f rawvideo " .. - df.sanitize_filename(sdr .. ".raw") - if not execute_cmd(cmd, string.format(_("Error generating %s"), sdr .. ".raw")) then + df.sanitize_filename(sdr_raw) + if not execute_cmd(cmd, string.format(_("Error generating %s"), sdr_raw)) then return cleanup(), errors end cmd = settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(hdr) .. resize_cmd .. - " -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(hdr .. ".raw") - if not execute_cmd(cmd, string.format(_("Error generating %s"), hdr .. ".raw")) then + " -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(hdr_raw) + if not execute_cmd(cmd, string.format(_("Error generating %s"), hdr_raw)) then + return cleanup(), errors + end + -- sanity check for file sizes (sometimes dt exports different size images if the files were never opened in darktable view) + if file_size(sdr_raw) ~= size_in_px * 4 or file_size(hdr_raw) ~= size_in_px & 3 then + table.insert(errors, string.format(_("Wrong raw image dimensions: %s, expected %dx%d. Try opening the image in darktable mode first."), images["sdr"].filename, sdr_w, sdr_h)) return cleanup(), errors end update_job_progress() - cmd = settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr .. ".raw") .. " -p " .. - df.sanitize_filename(hdr .. ".raw") .. + cmd = settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr_raw) .. " -p " .. + df.sanitize_filename(hdr_raw) .. string.format(" -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -s 1 -q %d -Q %d -D 1 ", settings.quality, settings.quality) .. " -w " .. tostring(sdr_w - sdr_w % 2) .. " -h " .. tostring(sdr_h - sdr_h % 2) .. " -z " .. df.sanitize_filename(uhdr) @@ -566,17 +588,18 @@ local function main() end local stacks, stack_count = get_stacks(dt.gui.selection(), encoding_variant, selection_type) - dt.print(string.format(_("Detected %d image stack(s)"), stack_count)) if stack_count == 0 then + dt.print(string.format(_("No image stacks detected.\n\nMake sure that the image pairs have the same widths and heights."), stack_count)) return end + dt.print(string.format(_("Detected %d image stack(s)"), stack_count)) job = dt.gui.create_job(_("Generating UltraHDR images"), true, stop_job) local count = 0 local msg for i, v in pairs(stacks) do local ok, errors = generate_ultrahdr(encoding_variant, v, settings, count, stack_count) if not ok then - dt.print(string.format(_("Errors generating images:\n\n- %s"), table.concat(errors, "\n- "))) + dt.print(string.format(_("Generating UltraHDR images failed:\n\n- %s"), table.concat(errors, "\n- "))) job.valid = false return end From a3404b64deb80df6cf482d2aea8bebeb78686a54 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Thu, 26 Sep 2024 10:44:14 +0200 Subject: [PATCH 31/41] Renamed gainmap to "gain map" in UI. --- contrib/ultrahdr.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 74baab9..abe9da9 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -20,7 +20,7 @@ ]] --[[ ULTRAHDR -Generate UltraHDR JPEG images from various combinations of source files (SDR, HDR, gainmap). +Generate UltraHDR JPEG images from various combinations of source files (SDR, HDR, gain map). https://developer.android.com/media/platform/hdr-image-format @@ -658,7 +658,7 @@ GUI.optionwidgets.output_settings_box = dt.new_widget("box") { } GUI.optionwidgets.metadata_label = dt.new_widget("label") { - label = _("gainmap metadata") + label = _("gain map metadata") } GUI.optionwidgets.min_content_boost = dt.new_widget("slider") { @@ -732,11 +732,11 @@ This will determine the method used to generate UltraHDR. - %s: SDR image paired with a gain map image. - %s: SDR image paired with an HDR image. -- %s: Each stack consists of a single SDR image. Gainmaps will be copies of SDR images. +- %s: Each stack consists of a single SDR image. Gain maps will be copies of SDR images. -By default, the first image in a stack is treated as SDR, and the second one is a gainmap/HDR. +By default, the first image in a stack is treated as SDR, and the second one is a gain map/HDR. You can force the image into a specific stack slot by attaching "hdr" / "gainmap" tags to it. -]]), _("SDR + gainmap"), _("SDR + HDR"), _("SDR only")), +]]), _("SDR + gain map"), _("SDR + HDR"), _("SDR only")), selected = 0, changed_callback = function(self) GUI.run.sensitive = self.selected and self.selected > 0 @@ -746,7 +746,7 @@ You can force the image into a specific stack slot by attaching "hdr" / "gainmap GUI.optionwidgets.metadata_box.visible = false end end, - _("SDR + gainmap"), -- ENCODING_VARIANT_SDR_AND_GAINMAP + _("SDR + gain map"), -- ENCODING_VARIANT_SDR_AND_GAINMAP _("SDR + HDR"), -- ENCODING_VARIANT_SDR_AND_HDR _("SDR only") -- ENCODING_VARIANT_SDR_AUTO_GAINMAP } From e4da226661da5ac2c4a35867d351da8cbeef36f7 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Thu, 26 Sep 2024 10:45:03 +0200 Subject: [PATCH 32/41] Added gainmap downsampling. --- contrib/ultrahdr.lua | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index abe9da9..261a886 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -77,7 +77,8 @@ local GUI = { metadata_box = {}, edit_executables_button = {}, executable_path_widget = {}, - quality_widget = {} + quality_widget = {}, + gainmap_downsampling_widget = {}, }, options = {}, run = {} @@ -145,6 +146,7 @@ local function save_preferences() dt.preferences.write(namespace, "hdr_capacity_max", "float", GUI.optionwidgets.hdr_capacity_max.value) end dt.preferences.write(namespace, "quality", "integer", GUI.optionwidgets.quality_widget.value) + dt.preferences.write(namespace, "gainmap_downsampling", "integer", GUI.optionwidgets.gainmap_downsampling_widget.value) end local function default_to(value, default) @@ -177,6 +179,7 @@ local function load_preferences() GUI.optionwidgets.hdr_capacity_max.value = default_to(dt.preferences.read(namespace, "hdr_capacity_max", "float"), 6.0) GUI.optionwidgets.quality_widget.value = default_to(dt.preferences.read(namespace, "quality", "integer"), 95) + GUI.optionwidgets.gainmap_downsampling_widget.value = default_to(dt.preferences.read(namespace, "gainmap_downsampling", "integer"), 0) end -- Changes the combobox selection blindly until a paired config value is set. @@ -221,6 +224,7 @@ local function assert_settings_correct(encoding_variant) hdr_capacity_max = GUI.optionwidgets.hdr_capacity_max.value }, quality = GUI.optionwidgets.quality_widget.value, + downsample = 2^GUI.optionwidgets.gainmap_downsampling_widget.value, tmpdir = dt.configuration.tmp_dir, skip_cleanup = false -- keep temporary files around, for debugging. } @@ -531,7 +535,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total cmd = settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr_raw) .. " -p " .. df.sanitize_filename(hdr_raw) .. string.format(" -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -s 1 -q %d -Q %d -D 1 ", settings.quality, - settings.quality) .. " -w " .. tostring(sdr_w - sdr_w % 2) .. " -h " .. tostring(sdr_h - sdr_h % 2) .. + settings.quality) .. string.format(" -s %d ", settings.downsample) .. " -w " .. tostring(sdr_w - sdr_w % 2) .. " -h " .. tostring(sdr_h - sdr_h % 2) .. " -z " .. df.sanitize_filename(uhdr) if not execute_cmd(cmd, string.format(_("Error merging %s"), uhdr)) then return cleanup(), errors @@ -742,8 +746,10 @@ You can force the image into a specific stack slot by attaching "hdr" / "gainmap GUI.run.sensitive = self.selected and self.selected > 0 if self.selected == ENCODING_VARIANT_SDR_AND_GAINMAP or self.selected == ENCODING_VARIANT_SDR_AUTO_GAINMAP then GUI.optionwidgets.metadata_box.visible = true + GUI.optionwidgets.gainmap_downsampling_widget.visible = false else GUI.optionwidgets.metadata_box.visible = false + GUI.optionwidgets.gainmap_downsampling_widget.visible = true end end, _("SDR + gain map"), -- ENCODING_VARIANT_SDR_AND_GAINMAP @@ -781,11 +787,26 @@ GUI.optionwidgets.quality_widget = dt.new_widget("slider") { end } +GUI.optionwidgets.gainmap_downsampling_widget = dt.new_widget("slider") { + label = _('gain map downsampling steps'), + tooltip = _('Exponent (2^x) of the gain map downsampling factor.\nDownsampling reduces the file size, at the expense of quality.\n\n0 = don\'t downsample the gain map, 7 = maximum downsampling (128x)'), + hard_min = 0, + hard_max = 7, + soft_min = 0, + soft_max = 7, + step = 1, + digits = 0, + reset_callback = function(self) + self.value = 0 + end +} + GUI.optionwidgets.encoding_settings_box = dt.new_widget("box") { orientation = "vertical", GUI.optionwidgets.selection_type_combo, GUI.optionwidgets.encoding_variant_combo, GUI.optionwidgets.quality_widget, + GUI.optionwidgets.gainmap_downsampling_widget, GUI.optionwidgets.metadata_box } From 72ce0b0f937d7d823a597e1a5c888ec61d4dba6c Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Thu, 24 Oct 2024 08:13:49 +0200 Subject: [PATCH 33/41] Small fixes --- contrib/ultrahdr.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 261a886..e17c2a6 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -484,7 +484,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total ok = copy_or_export(images["hdr"], hdr, "jpegxl", DT_COLORSPACE_PQ_P3, { bpp = 10, quality = 100, -- lossless - effort = 1 -- we don't care about the size, the faile is temporary. + effort = 1 -- we don't care about the size, the file is temporary. }) if not ok then table.insert(errors, string.format(_("Error exporting %s to %s"), images["hdr"].filename, "jxl")) @@ -527,8 +527,8 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total return cleanup(), errors end -- sanity check for file sizes (sometimes dt exports different size images if the files were never opened in darktable view) - if file_size(sdr_raw) ~= size_in_px * 4 or file_size(hdr_raw) ~= size_in_px & 3 then - table.insert(errors, string.format(_("Wrong raw image dimensions: %s, expected %dx%d. Try opening the image in darktable mode first."), images["sdr"].filename, sdr_w, sdr_h)) + if file_size(sdr_raw) ~= size_in_px * 4 or file_size(hdr_raw) ~= size_in_px * 3 then + table.insert(errors, string.format(_("Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first."), images["sdr"].filename, sdr_w, sdr_h)) return cleanup(), errors end update_job_progress() @@ -766,7 +766,7 @@ This determines how the plugin groups images into separate stacks (each stack wi - %s: Group images into stacks, using the source image path + filename (ignoring extension). Use this method if the source images for a given stack are darktable duplicates. -As an added precaution, each image in a stack needs to have the same dimensions. +As an added precaution, each image in a stack needs to have the same resolution. ]]), _("one stack"), _("multiple stacks (use filename)")), selected = 0, _("one stack"), -- SELECTION_TYPE_ONE_STACK @@ -789,7 +789,7 @@ GUI.optionwidgets.quality_widget = dt.new_widget("slider") { GUI.optionwidgets.gainmap_downsampling_widget = dt.new_widget("slider") { label = _('gain map downsampling steps'), - tooltip = _('Exponent (2^x) of the gain map downsampling factor.\nDownsampling reduces the file size, at the expense of quality.\n\n0 = don\'t downsample the gain map, 7 = maximum downsampling (128x)'), + tooltip = _('Exponent (2^x) of the gain map downsampling factor.\nDownsampling reduces the gain map resolution.\n\n0 = don\'t downsample the gain map, 7 = maximum downsampling (128x)'), hard_min = 0, hard_max = 7, soft_min = 0, From 95e2e4924ef2942ad0421403c2f1e682b03a0897 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Mon, 25 Nov 2024 17:14:30 +0100 Subject: [PATCH 34/41] Added HDR only encoding variant. - Increase timeouts to improve stability of choosing export profiles. - Added peak nits setting. --- contrib/ultrahdr.lua | 192 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 157 insertions(+), 35 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index e17c2a6..35d0203 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -79,6 +79,7 @@ local GUI = { executable_path_widget = {}, quality_widget = {}, gainmap_downsampling_widget = {}, + target_display_peak_nits_widget = {} }, options = {}, run = {} @@ -100,6 +101,7 @@ local PS = dt.configuration.running_os == "windows" and "\\" or "/" local ENCODING_VARIANT_SDR_AND_GAINMAP = 1 local ENCODING_VARIANT_SDR_AND_HDR = 2 local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3 +local ENCODING_VARIANT_HDR_ONLY = 4 local SELECTION_TYPE_ONE_STACK = 1 local SELECTION_TYPE_GROUP_BY_FNAME = 2 @@ -146,7 +148,11 @@ local function save_preferences() dt.preferences.write(namespace, "hdr_capacity_max", "float", GUI.optionwidgets.hdr_capacity_max.value) end dt.preferences.write(namespace, "quality", "integer", GUI.optionwidgets.quality_widget.value) - dt.preferences.write(namespace, "gainmap_downsampling", "integer", GUI.optionwidgets.gainmap_downsampling_widget.value) + dt.preferences.write(namespace, "gainmap_downsampling", "integer", + GUI.optionwidgets.gainmap_downsampling_widget.value) + dt.preferences.write(namespace, "target_display_peak_nits", "integer", + (GUI.optionwidgets.target_display_peak_nits_widget.value+0.5)//1) + end local function default_to(value, default) @@ -179,7 +185,10 @@ local function load_preferences() GUI.optionwidgets.hdr_capacity_max.value = default_to(dt.preferences.read(namespace, "hdr_capacity_max", "float"), 6.0) GUI.optionwidgets.quality_widget.value = default_to(dt.preferences.read(namespace, "quality", "integer"), 95) - GUI.optionwidgets.gainmap_downsampling_widget.value = default_to(dt.preferences.read(namespace, "gainmap_downsampling", "integer"), 0) + GUI.optionwidgets.target_display_peak_nits_widget.value = default_to( + dt.preferences.read(namespace, "target_display_peak_nits", "integer"), 10000) + GUI.optionwidgets.gainmap_downsampling_widget.value = default_to( + dt.preferences.read(namespace, "gainmap_downsampling", "integer"), 0) end -- Changes the combobox selection blindly until a paired config value is set. @@ -192,11 +201,12 @@ local function set_combobox(path, instance, config_name, new_config_value) end dt.gui.action(path, 0, "selection", "first", 1.0) + dt.control.sleep(50) local limit, i = 30, 0 -- in case there is no matching config value in the first n entries of a combobox. while i < limit do i = i + 1 dt.gui.action(path, 0, "selection", "next", 1.0) - dt.control.sleep(10) + dt.control.sleep(50) if dt.preferences.read("darktable", config_name, "integer") == new_config_value then log.msg(log.debug, string.format(_("Changed %s from %d to %d"), config_name, pref, new_config_value)) return pref @@ -224,7 +234,8 @@ local function assert_settings_correct(encoding_variant) hdr_capacity_max = GUI.optionwidgets.hdr_capacity_max.value }, quality = GUI.optionwidgets.quality_widget.value, - downsample = 2^GUI.optionwidgets.gainmap_downsampling_widget.value, + target_display_peak_nits = (GUI.optionwidgets.target_display_peak_nits_widget.value+0.5)//1, + downsample = 2 ^ GUI.optionwidgets.gainmap_downsampling_widget.value, tmpdir = dt.configuration.tmp_dir, skip_cleanup = false -- keep temporary files around, for debugging. } @@ -263,23 +274,27 @@ end local function get_stacks(images, encoding_variant, selection_type) local stacks = {} - local extra_image_content_type + local primary = "sdr" + local extra if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then - extra_image_content_type = "gainmap" + extra = "gainmap" elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then - extra_image_content_type = "hdr" + extra = "hdr" elseif encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then - extra_image_content_type = nil + extra = nil + elseif encoding_variant == ENCODING_VARIANT_HDR_ONLY then + extra = nil + primary = "hdr" end local tags = nil - -- Group images into (sdr [,extra]) stacks - -- Assume that the first encountered image from each stack is an sdr one, unless it has a tag matching the expected extra_image_type, or has the expected extension + -- Group images into (primary [,extra]) stacks + -- Assume that the first encountered image from each stack is a primary one, unless it has a tag matching the expected extra_image_type, or has the expected extension for k, v in pairs(images) do local is_extra = false tags = dt.tags.get_tags(v) for ignore, tag in pairs(tags) do - if extra_image_content_type and tag.name == extra_image_content_type then + if extra and tag.name == extra then is_extra = true end end @@ -296,27 +311,27 @@ local function get_stacks(images, encoding_variant, selection_type) if stacks[key] == nil then stacks[key] = {} end - if extra_image_content_type and (is_extra or stacks[key]["sdr"]) then + if extra and (is_extra or stacks[key][primary]) then -- Don't overwrite existing entries - if not stacks[key][extra_image_content_type] then - stacks[key][extra_image_content_type] = v + if not stacks[key][extra] then + stacks[key][extra] = v end elseif not is_extra then -- Don't overwrite existing entries - if not stacks[key]["sdr"] then - stacks[key]["sdr"] = v + if not stacks[key][primary] then + stacks[key][primary] = v end end end -- remove invalid stacks local count = 0 for k, v in pairs(stacks) do - if extra_image_content_type then - if not v["sdr"] or not v[extra_image_content_type] then + if extra then + if not v[primary] or not v[extra] then stacks[k] = nil else - local sdr_w, sdr_h = get_dimensions(v["sdr"]) - local extra_w, extra_h = get_dimensions(v[extra_image_content_type]) + local sdr_w, sdr_h = get_dimensions(v[primary]) + local extra_w, extra_h = get_dimensions(v[extra]) if (sdr_w ~= extra_w) or (sdr_h ~= extra_h) then stacks[k] = nil end @@ -346,6 +361,7 @@ end local function generate_ultrahdr(encoding_variant, images, settings, step, total_steps) local total_substeps local substep = 0 + local best_source_image local uhdr local errors = {} local remove_files = {} @@ -408,6 +424,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then total_substeps = 5 + best_source_image = images["sdr"] -- Export/copy both SDR and gainmap to JPEGs local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) .. ".jpg") @@ -456,9 +473,13 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total -- Merge files uhdr = df.chop_filetype(sdr) .. "_ultrahdr.jpg" table.insert(remove_files, uhdr) - cmd = settings.bin.ultrahdr_app .. " -m 0 -i " .. df.sanitize_filename(sdr .. ".noexif") .. " -g " .. - df.sanitize_filename(gainmap) .. " -f " .. df.sanitize_filename(metadata_file) .. " -z " .. - df.sanitize_filename(uhdr) + cmd = settings.bin.ultrahdr_app .. + string.format(" -m 0 -i %s -g %s -L %d -f %s -z %s", df.sanitize_filename(sdr .. ".noexif"), -- -i + df.sanitize_filename(gainmap), -- -g + settings.target_display_peak_nits, -- -L + df.sanitize_filename(metadata_file), -- -f + df.sanitize_filename(uhdr) -- -z + ) if not execute_cmd(cmd, string.format(_("Error merging UltraHDR to %s"), uhdr)) then return cleanup(), errors end @@ -476,6 +497,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total update_job_progress() elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then total_substeps = 6 + best_source_image = images["sdr"] -- https://discuss.pixls.us/t/manual-creation-of-ultrahdr-images/45004/20 -- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3 local hdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) .. @@ -528,15 +550,26 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total end -- sanity check for file sizes (sometimes dt exports different size images if the files were never opened in darktable view) if file_size(sdr_raw) ~= size_in_px * 4 or file_size(hdr_raw) ~= size_in_px * 3 then - table.insert(errors, string.format(_("Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first."), images["sdr"].filename, sdr_w, sdr_h)) + table.insert(errors, + string.format( + _("Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first."), + images["sdr"].filename, sdr_w, sdr_h)) return cleanup(), errors end update_job_progress() - cmd = settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr_raw) .. " -p " .. - df.sanitize_filename(hdr_raw) .. - string.format(" -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -s 1 -q %d -Q %d -D 1 ", settings.quality, - settings.quality) .. string.format(" -s %d ", settings.downsample) .. " -w " .. tostring(sdr_w - sdr_w % 2) .. " -h " .. tostring(sdr_h - sdr_h % 2) .. - " -z " .. df.sanitize_filename(uhdr) + cmd = settings.bin.ultrahdr_app .. + string.format( + " -m 0 -y %s -p %s -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -q %d -Q %d -L %d -D 1 -s %d -w %d -h %d -z %s", + df.sanitize_filename(sdr_raw), -- -y + df.sanitize_filename(hdr_raw), -- -p + settings.quality, -- -q + settings.quality, -- -Q + settings.target_display_peak_nits, -- -L + settings.downsample, -- -s + sdr_w - sdr_w % 2, -- w + sdr_h - sdr_h % 2, -- h + df.sanitize_filename(uhdr) -- z + ) if not execute_cmd(cmd, string.format(_("Error merging %s"), uhdr)) then return cleanup(), errors end @@ -551,13 +584,82 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total end end update_job_progress() + elseif encoding_variant == ENCODING_VARIANT_HDR_ONLY then + total_substeps = 5 + best_source_image = images["hdr"] + -- TODO: Check if exporting to JXL would be ok too. + -- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3 + local hdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) .. + ".jxl") + table.insert(remove_files, hdr) + ok = copy_or_export(images["hdr"], hdr, "jpegxl", DT_COLORSPACE_PQ_P3, { + bpp = 10, + quality = 100, -- lossless + effort = 1 -- we don't care about the size, the file is temporary. + }) + if not ok then + table.insert(errors, string.format(_("Error exporting %s to %s"), images["hdr"].filename, "jxl")) + return cleanup(), errors + end + update_job_progress() + -- Step 1: Generate raw HDR image + local hdr_raw = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) .. + ".raw") + table.insert(remove_files, hdr_raw) + local hdr_w, hdr_h = get_dimensions(images["hdr"]) + local resize_cmd = "" + if hdr_h % 2 + hdr_w % 2 > 0 then -- needs resizing to even dimensions. + resize_cmd = string.format(" -vf 'crop=%d:%d:0:0' ", hdr_w - hdr_w % 2, hdr_h - hdr_h % 2) + end + local size_in_px = (hdr_w - hdr_w % 2) * (hdr_h - hdr_h % 2) + cmd = settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(hdr) .. resize_cmd .. + " -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(hdr_raw) + if not execute_cmd(cmd, string.format(_("Error generating %s"), hdr_raw)) then + return cleanup(), errors + end + if file_size(hdr_raw) ~= size_in_px * 3 then + table.insert(errors, + string.format( + _("Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first."), + images["hdr"].filename, hdr_w, hdr_h)) + return cleanup(), errors + end + update_job_progress() + uhdr = df.chop_filetype(hdr_raw) .. "_ultrahdr.jpg" + table.insert(remove_files, uhdr) + cmd = settings.bin.ultrahdr_app .. + string.format( + " -m 0 -p %s -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -q %d -Q %d -D 1 -L %d -s %d -w %d -h %d -z %s", + df.sanitize_filename(hdr_raw), -- -p + settings.quality, -- -q + settings.quality, -- -Q + settings.target_display_peak_nits, -- -L + settings.downsample, -- s + hdr_w - hdr_w % 2, -- -w + hdr_h - hdr_h % 2, -- -h + df.sanitize_filename(uhdr) -- -z + ) + if not execute_cmd(cmd, string.format(_("Error merging %s"), uhdr)) then + return cleanup(), errors + end + update_job_progress() + if settings.copy_exif then + -- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all). + -- This might hapen e.g. when the source files are Adobe gainmap HDRs. + cmd = settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(hdr) .. " -exif " .. + df.sanitize_filename(uhdr) .. " -overwrite_original -preserve" + if not execute_cmd(cmd, string.format(_("Error adding EXIF to %s"), uhdr)) then + return cleanup(), errors + end + end + update_job_progress() end - local output_dir = settings.use_original_dir and images["sdr"].path or settings.output + local output_dir = settings.use_original_dir and best_source_image.path or settings.output local output_file = df.create_unique_filename(output_dir .. PS .. df.get_filename(uhdr)) ok = df.file_move(uhdr, output_file) if not ok then - table.insert(errors, string.format(_("Error generating UltraHDR for %s"), images["sdr"].filename)) + table.insert(errors, string.format(_("Error generating UltraHDR for %s"), best_source_image.filename)) return cleanup(), errors end if settings.import_to_darktable then @@ -593,7 +695,9 @@ local function main() local stacks, stack_count = get_stacks(dt.gui.selection(), encoding_variant, selection_type) if stack_count == 0 then - dt.print(string.format(_("No image stacks detected.\n\nMake sure that the image pairs have the same widths and heights."), stack_count)) + dt.print(string.format(_( + "No image stacks detected.\n\nMake sure that the image pairs have the same widths and heights."), + stack_count)) return end dt.print(string.format(_("Detected %d image stack(s)"), stack_count)) @@ -737,10 +841,11 @@ This will determine the method used to generate UltraHDR. - %s: SDR image paired with a gain map image. - %s: SDR image paired with an HDR image. - %s: Each stack consists of a single SDR image. Gain maps will be copies of SDR images. +- %s: Each stack consists of a single HDR image. HDR will be tone mapped to SDR. By default, the first image in a stack is treated as SDR, and the second one is a gain map/HDR. You can force the image into a specific stack slot by attaching "hdr" / "gainmap" tags to it. -]]), _("SDR + gain map"), _("SDR + HDR"), _("SDR only")), +]]), _("SDR + gain map"), _("SDR + HDR"), _("SDR only"), _("HDR only")), selected = 0, changed_callback = function(self) GUI.run.sensitive = self.selected and self.selected > 0 @@ -754,7 +859,8 @@ You can force the image into a specific stack slot by attaching "hdr" / "gainmap end, _("SDR + gain map"), -- ENCODING_VARIANT_SDR_AND_GAINMAP _("SDR + HDR"), -- ENCODING_VARIANT_SDR_AND_HDR - _("SDR only") -- ENCODING_VARIANT_SDR_AUTO_GAINMAP + _("SDR only"), -- ENCODING_VARIANT_SDR_AUTO_GAINMAP + _("HDR only") -- ENCODING_VARIANT_HDR_ONLY } GUI.optionwidgets.selection_type_combo = dt.new_widget("combobox") { @@ -787,9 +893,24 @@ GUI.optionwidgets.quality_widget = dt.new_widget("slider") { end } +GUI.optionwidgets.target_display_peak_nits_widget = dt.new_widget("slider") { + label = _('target display peak brightness (nits)'), + tooltip = _('Peak brightness of target display in nits (defaults to 10000)'), + hard_min = 203, + hard_max = 10000, + soft_min = 1000, + soft_max = 10000, + step = 10, + digits = 0, + reset_callback = function(self) + self.value = 10000 + end +} + GUI.optionwidgets.gainmap_downsampling_widget = dt.new_widget("slider") { label = _('gain map downsampling steps'), - tooltip = _('Exponent (2^x) of the gain map downsampling factor.\nDownsampling reduces the gain map resolution.\n\n0 = don\'t downsample the gain map, 7 = maximum downsampling (128x)'), + tooltip = _( + 'Exponent (2^x) of the gain map downsampling factor.\nDownsampling reduces the gain map resolution.\n\n0 = don\'t downsample the gain map, 7 = maximum downsampling (128x)'), hard_min = 0, hard_max = 7, soft_min = 0, @@ -807,6 +928,7 @@ GUI.optionwidgets.encoding_settings_box = dt.new_widget("box") { GUI.optionwidgets.encoding_variant_combo, GUI.optionwidgets.quality_widget, GUI.optionwidgets.gainmap_downsampling_widget, + GUI.optionwidgets.target_display_peak_nits_widget, GUI.optionwidgets.metadata_box } From a09140c03c475bd9d0deb51645574ddf5172550f Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Mon, 2 Dec 2024 23:40:15 +0100 Subject: [PATCH 35/41] Variable substitution in output directory. Force exporting files, but left the code to reconfigure it. Added more robust way to set export profile. --- contrib/ultrahdr.lua | 100 +++++++++++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 38 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 35d0203..111c521 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -39,6 +39,7 @@ USAGE ]] local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" +local ds = require "lib/dtutils.string" local log = require "lib/dtutils.log" local dtsys = require "lib/dtutils.system" local dd = require "lib/dtutils.debug" @@ -65,8 +66,9 @@ local GUI = { encoding_settings_box = {}, output_settings_label = {}, output_settings_box = {}, - use_original_directory = {}, - output_directory_widget = {}, + output_filepath_label = {}, + output_filepath_widget = {}, + overwrite_on_conflict = {}, copy_exif = {}, import_to_darktable = {}, min_content_boost = {}, @@ -110,6 +112,12 @@ local SELECTION_TYPE_GROUP_BY_FNAME = 2 local DT_COLORSPACE_PQ_P3 = 24 local DT_COLORSPACE_DISPLAY_P3 = 26 +-- 1-based position of a colorspace in export profile combobox. +local COLORSPACE_TO_GUI_ACTION = { + [DT_COLORSPACE_PQ_P3] = 9, + [DT_COLORSPACE_DISPLAY_P3] = 11 +} + local function generate_metadata_file(settings) local metadata_file_fmt = [[--maxContentBoost %f --minContentBoost %f @@ -135,10 +143,8 @@ end local function save_preferences() dt.preferences.write(namespace, "encoding_variant", "integer", GUI.optionwidgets.encoding_variant_combo.selected) dt.preferences.write(namespace, "selection_type", "integer", GUI.optionwidgets.selection_type_combo.selected) - dt.preferences.write(namespace, "use_original_directory", "bool", GUI.optionwidgets.use_original_directory.value) - if GUI.optionwidgets.output_directory_widget.value then - dt.preferences.write(namespace, "output_directory", "string", GUI.optionwidgets.output_directory_widget.value) - end + dt.preferences.write(namespace, "output_filepath_pattern", "string", GUI.optionwidgets.output_filepath_widget.text) + dt.preferences.write(namespace, "overwrite_on_conflict", "bool", GUI.optionwidgets.overwrite_on_conflict.value) dt.preferences.write(namespace, "import_to_darktable", "bool", GUI.optionwidgets.import_to_darktable.value) dt.preferences.write(namespace, "copy_exif", "bool", GUI.optionwidgets.copy_exif.value) if GUI.optionwidgets.min_content_boost.value then @@ -151,7 +157,7 @@ local function save_preferences() dt.preferences.write(namespace, "gainmap_downsampling", "integer", GUI.optionwidgets.gainmap_downsampling_widget.value) dt.preferences.write(namespace, "target_display_peak_nits", "integer", - (GUI.optionwidgets.target_display_peak_nits_widget.value+0.5)//1) + (GUI.optionwidgets.target_display_peak_nits_widget.value + 0.5) // 1) end @@ -169,11 +175,8 @@ local function load_preferences() GUI.optionwidgets.selection_type_combo.selected = math.max( dt.preferences.read(namespace, "selection_type", "integer"), SELECTION_TYPE_ONE_STACK) - GUI.optionwidgets.output_directory_widget.value = dt.preferences.read(namespace, "output_directory", "string") - GUI.optionwidgets.use_original_directory.value = dt.preferences.read(namespace, "use_original_directory", "bool") - if not GUI.optionwidgets.output_directory_widget.value then - GUI.optionwidgets.use_original_directory.value = true - end + GUI.optionwidgets.output_filepath_widget.text = dt.preferences.read(namespace, "output_filepath_pattern", "string") + GUI.optionwidgets.overwrite_on_conflict.value = dt.preferences.read(namespace, "overwrite_on_conflict", "bool") GUI.optionwidgets.import_to_darktable.value = dt.preferences.read(namespace, "import_to_darktable", "bool") GUI.optionwidgets.copy_exif.value = dt.preferences.read(namespace, "copy_exif", "bool") GUI.optionwidgets.min_content_boost.value = default_to(dt.preferences.read(namespace, "min_content_boost", "float"), @@ -191,10 +194,25 @@ local function load_preferences() dt.preferences.read(namespace, "gainmap_downsampling", "integer"), 0) end +local function set_profile(colorspace) + local set_directly = true + + if set_directly then + -- New method, with hardcoded export profile values. + local old = dt.gui.action("lib/export/profile", 0, "selection", "", "") * -1 + local new = COLORSPACE_TO_GUI_ACTION[colorspace] or colorspace + log.msg(log.debug, string.format("%d %d %d %d", colorspace, new, old, new - old)) + dt.gui.action("lib/export/profile", 0, "selection", "next", new - old) + return old + else + -- Old method, timing-dependent + return set_combobox("lib/export/profile", 0, "plugins/lighttable/export/icctype", colorspace) + end +end + -- Changes the combobox selection blindly until a paired config value is set. -- Workaround for https://github.com/darktable-org/lua-scripts/issues/522 local function set_combobox(path, instance, config_name, new_config_value) - local pref = dt.preferences.read("darktable", config_name, "integer") if pref == new_config_value then return new_config_value @@ -223,8 +241,8 @@ local function assert_settings_correct(encoding_variant) exiftool = df.check_if_bin_exists("exiftool"), ffmpeg = df.check_if_bin_exists("ffmpeg") }, - output = GUI.optionwidgets.output_directory_widget.value, - use_original_dir = GUI.optionwidgets.use_original_directory.value, + overwrite_on_conflict = GUI.optionwidgets.overwrite_on_conflict.value, + output_filepath_pattern = GUI.optionwidgets.output_filepath_widget.text, import_to_darktable = GUI.optionwidgets.import_to_darktable.value, copy_exif = GUI.optionwidgets.copy_exif.value, metadata = { @@ -234,16 +252,13 @@ local function assert_settings_correct(encoding_variant) hdr_capacity_max = GUI.optionwidgets.hdr_capacity_max.value }, quality = GUI.optionwidgets.quality_widget.value, - target_display_peak_nits = (GUI.optionwidgets.target_display_peak_nits_widget.value+0.5)//1, + target_display_peak_nits = (GUI.optionwidgets.target_display_peak_nits_widget.value + 0.5) // 1, downsample = 2 ^ GUI.optionwidgets.gainmap_downsampling_widget.value, tmpdir = dt.configuration.tmp_dir, - skip_cleanup = false -- keep temporary files around, for debugging. + skip_cleanup = false, -- keep temporary files around, for debugging. + force_export = true -- if false, will copy source files instead of exporting if the file extension matches the format expectation. } - if not settings.use_original_dir and (not settings.output or not df.check_if_file_exists(settings.output)) then - table.insert(errors, string.format(_("output directory (%s) not found"), settings.output)) - end - for k, v in pairs(settings.bin) do if not v then table.insert(errors, string.format(_("%s binary not found"), k)) @@ -387,10 +402,11 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total end function copy_or_export(src_image, dest, format, colorspace, props) - if df.get_filetype(src_image.filename) == df.get_filetype(dest) and not src_image.is_altered then + if not settings.force_export and df.get_filetype(src_image.filename) == df.get_filetype(dest) and + not src_image.is_altered then return df.file_copy(src_image.path .. PS .. src_image.filename, dest) else - local prev = set_combobox("lib/export/profile", 0, "plugins/lighttable/export/icctype", colorspace) + local prev = set_profile(colorspace) if not prev then return false end @@ -405,7 +421,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total end if prev then - set_combobox("lib/export/profile", 0, "plugins/lighttable/export/icctype", prev) + set_profile(prev) end return ok end @@ -655,8 +671,10 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total update_job_progress() end - local output_dir = settings.use_original_dir and best_source_image.path or settings.output - local output_file = df.create_unique_filename(output_dir .. PS .. df.get_filename(uhdr)) + local output_file = ds.substitute(best_source_image, step + 1, settings.output_filepath_pattern) .. ".jpg" + if not settings.overwrite_on_conflict then + output_file = df.create_unique_filename(output_file) + end ok = df.file_move(uhdr, output_file) if not ok then table.insert(errors, string.format(_("Error generating UltraHDR for %s"), best_source_image.filename)) @@ -733,17 +751,20 @@ GUI.optionwidgets.output_settings_label = dt.new_widget("section_label") { label = _("output") } -GUI.optionwidgets.output_directory_widget = dt.new_widget("file_chooser_button") { - title = _("select directory to write UltraHDR image files to"), - is_directory = true +GUI.optionwidgets.output_filepath_label = dt.new_widget("label") { + label = _("file path pattern"), + tooltip = ds.get_substitution_tooltip() } -GUI.optionwidgets.use_original_directory = dt.new_widget("check_button") { - label = _("export to original directory"), - tooltip = _("Write UltraHDR images to the same directory as their original images"), - clicked_callback = function(self) - GUI.optionwidgets.output_directory_widget.sensitive = not self.value - end +GUI.optionwidgets.output_filepath_widget = dt.new_widget("entry") { + tooltip = ds.get_substitution_tooltip(), + placeholder = _("e.g. $(FILE_FOLDER)/$(FILE_NAME)_ultrahdr") +} + +GUI.optionwidgets.overwrite_on_conflict = dt.new_widget("check_button") { + label = _("overwrite if exists"), + tooltip = _( + "If the output file already exists, overwrite it. If unchecked, a unique filename will be created instead.") } GUI.optionwidgets.import_to_darktable = dt.new_widget("check_button") { @@ -759,8 +780,9 @@ GUI.optionwidgets.copy_exif = dt.new_widget("check_button") { GUI.optionwidgets.output_settings_box = dt.new_widget("box") { orientation = "vertical", GUI.optionwidgets.output_settings_label, - GUI.optionwidgets.use_original_directory, - GUI.optionwidgets.output_directory_widget, + GUI.optionwidgets.output_filepath_label, + GUI.optionwidgets.output_filepath_widget, + GUI.optionwidgets.overwrite_on_conflict, GUI.optionwidgets.import_to_darktable, GUI.optionwidgets.copy_exif } @@ -845,7 +867,9 @@ This will determine the method used to generate UltraHDR. By default, the first image in a stack is treated as SDR, and the second one is a gain map/HDR. You can force the image into a specific stack slot by attaching "hdr" / "gainmap" tags to it. -]]), _("SDR + gain map"), _("SDR + HDR"), _("SDR only"), _("HDR only")), + +For HDR source images, apply a log2(203 nits/10000 nits) = -5.62 EV exposure correction +before generating UltraHDR.]]), _("SDR + gain map"), _("SDR + HDR"), _("SDR only"), _("HDR only")), selected = 0, changed_callback = function(self) GUI.run.sensitive = self.selected and self.selected > 0 From 77c405882d9cca8f68163c67eca6b8ebad067922 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Tue, 3 Dec 2024 19:49:54 +0100 Subject: [PATCH 36/41] Added logging code and a workaround for https://github.com/darktable-org/darktable/issues/17528 in 9.4.0. --- .DS_Store | Bin 0 -> 6148 bytes contrib/ultrahdr.lua | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c6f7696114afdb45c3563536fccffc402bfedec9 GIT binary patch literal 6148 zcmeHKJ8DBQ5S&d6F}QJ=Qdh_ggmF&b3k2H)48{-g>0gy|< z4@cjUNdYM!1*Cu!kOCK0pbG4Kap7}yoD`4(*ROzo9~#}U7mkVX>EIA80CC1}80XPT z5Ss^xy>Luqgl0)4Ce^CNu%t8IDz6ugiAjf5^I>(fRfl46JI`;C4(o{;rGOMTSKv06 zORxWr^k4e_bCOn4KnnaT1#Gt7tXF(e)z-=5yw*1Q6Wwz@>290{g+r8MVw7Vpyd2+0 cQsy 0 then return nil, errors end @@ -288,6 +309,7 @@ local function get_dimensions(image) end local function get_stacks(images, encoding_variant, selection_type) + local old_log_level = set_log_level(LOG_LEVEL) local stacks = {} local primary = "sdr" local extra @@ -356,6 +378,7 @@ local function get_stacks(images, encoding_variant, selection_type) count = count + 1 end end + restore_log_level(old_log_level) return stacks, count end @@ -374,6 +397,7 @@ local function file_size(path) end local function generate_ultrahdr(encoding_variant, images, settings, step, total_steps) + local old_log_level = set_log_level(LOG_LEVEL) local total_substeps local substep = 0 local best_source_image @@ -402,6 +426,8 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total end function copy_or_export(src_image, dest, format, colorspace, props) + -- Workaround for https://github.com/darktable-org/darktable/issues/17528 + local needs_workaround = dt.configuration.api_version_string == "9.3.0" or dt.configuration.api_version_string == "9.4.0" if not settings.force_export and df.get_filetype(src_image.filename) == df.get_filetype(dest) and not src_image.is_altered then return df.file_copy(src_image.path .. PS .. src_image.filename, dest) @@ -415,11 +441,10 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total exporter[k] = v end local ok = exporter:write_image(src_image, dest) - if dt.configuration.api_version_string == "9.3.0" then - -- Workaround for https://github.com/darktable-org/darktable/issues/17528 + if needs_workaround then ok = not ok end - + log.msg(log.info, string.format("Exporting %s to %s (format: %s): %s", src_image.filename, dest, format, ok)) if prev then set_profile(prev) end @@ -695,10 +720,12 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total local msg = string.format(_("Generated %s."), df.get_filename(output_file)) log.msg(log.info, msg) dt.print(msg) + restore_log_level(old_log_level) return true, nil end local function main() + local old_log_level = set_log_level(LOG_LEVEL) save_preferences() local selection_type = GUI.optionwidgets.selection_type_combo.selected @@ -741,6 +768,7 @@ local function main() msg = string.format(_("Generated %d UltraHDR image(s)."), count) log.msg(log.info, msg) dt.print(msg) + restore_log_level(old_log_level) end GUI.optionwidgets.settings_label = dt.new_widget("section_label") { From 3bdbfd12bf7f76e7de148ef3c2024cfe8de34cc0 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Thu, 5 Dec 2024 00:45:34 +0100 Subject: [PATCH 37/41] Apply write_image workaround only in 9.3.0 --- contrib/ultrahdr.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 29b30d9..fc696de 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -427,7 +427,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total function copy_or_export(src_image, dest, format, colorspace, props) -- Workaround for https://github.com/darktable-org/darktable/issues/17528 - local needs_workaround = dt.configuration.api_version_string == "9.3.0" or dt.configuration.api_version_string == "9.4.0" + local needs_workaround = dt.configuration.api_version_string == "9.3.0" if not settings.force_export and df.get_filetype(src_image.filename) == df.get_filetype(dest) and not src_image.is_altered then return df.file_copy(src_image.path .. PS .. src_image.filename, dest) From 3c67c0c69304bbe229056ce01408750fb0818720 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Sat, 21 Dec 2024 10:20:02 +0100 Subject: [PATCH 38/41] Create output directory if it doesn't exist. --- contrib/ultrahdr.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index fc696de..ef7a61f 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -700,6 +700,8 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total if not settings.overwrite_on_conflict then output_file = df.create_unique_filename(output_file) end + local output_path = ds.get_path(output_file) + df.mkdir(output_path) ok = df.file_move(uhdr, output_file) if not ok then table.insert(errors, string.format(_("Error generating UltraHDR for %s"), best_source_image.filename)) From 6d16a8778b61aea1196d78ab901a1aff04eb2ca1 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Sun, 22 Dec 2024 08:39:38 +0100 Subject: [PATCH 39/41] Removed .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index c6f7696114afdb45c3563536fccffc402bfedec9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJ8DBQ5S&d6F}QJ=Qdh_ggmF&b3k2H)48{-g>0gy|< z4@cjUNdYM!1*Cu!kOCK0pbG4Kap7}yoD`4(*ROzo9~#}U7mkVX>EIA80CC1}80XPT z5Ss^xy>Luqgl0)4Ce^CNu%t8IDz6ugiAjf5^I>(fRfl46JI`;C4(o{;rGOMTSKv06 zORxWr^k4e_bCOn4KnnaT1#Gt7tXF(e)z-=5yw*1Q6Wwz@>290{g+r8MVw7Vpyd2+0 cQsy Date: Thu, 2 Jan 2025 15:21:59 +0100 Subject: [PATCH 40/41] Added 50ms sleep to the colorspace change. The old profile still seems to be used occasionally without the sleep. --- contrib/ultrahdr.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index ef7a61f..94df217 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -120,6 +120,7 @@ local COLORSPACE_TO_GUI_ACTION = { [DT_COLORSPACE_DISPLAY_P3] = 11 } +local UI_SLEEP_MS = 50 -- How many ms to sleep after UI action. local function set_log_level(level) local old_log_level = log.log_level() @@ -221,9 +222,10 @@ local function set_profile(colorspace) local new = COLORSPACE_TO_GUI_ACTION[colorspace] or colorspace log.msg(log.debug, string.format("Changing export profile from %d to %d", old, new)) dt.gui.action("lib/export/profile", 0, "selection", "next", new - old) + dt.control.sleep(UI_SLEEP_MS) return old else - -- Old method, timing-dependent + -- Old method return set_combobox("lib/export/profile", 0, "plugins/lighttable/export/icctype", colorspace) end end @@ -238,12 +240,12 @@ local function set_combobox(path, instance, config_name, new_config_value) end dt.gui.action(path, 0, "selection", "first", 1.0) - dt.control.sleep(50) + dt.control.sleep(UI_SLEEP_MS) local limit, i = 30, 0 -- in case there is no matching config value in the first n entries of a combobox. while i < limit do i = i + 1 dt.gui.action(path, 0, "selection", "next", 1.0) - dt.control.sleep(50) + dt.control.sleep(UI_SLEEP_MS) if dt.preferences.read("darktable", config_name, "integer") == new_config_value then log.msg(log.debug, string.format(_("Changed %s from %d to %d"), config_name, pref, new_config_value)) return pref From 9f3fb161613334f11c7542916997a28fe1818ab2 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotowicz Date: Sat, 4 Jan 2025 00:49:56 +0100 Subject: [PATCH 41/41] Addressed review comments. --- contrib/ultrahdr.lua | 52 +++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua index 94df217..e556232 100644 --- a/contrib/ultrahdr.lua +++ b/contrib/ultrahdr.lua @@ -45,15 +45,13 @@ local dtsys = require "lib/dtutils.system" local dd = require "lib/dtutils.debug" local gettext = dt.gettext.gettext -local namespace = "module_ultrahdr" +local namespace = "ultrahdr" -local LOG_LEVEL = log.info +local LOG_LEVEL = log.info -- works with darktable API version from 4.8.0 on du.check_min_api_version("9.3.0", "ultrahdr") -dt.gettext.bindtextdomain(namespace, dt.configuration.config_dir .. "/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -96,31 +94,32 @@ flags.module_installed = false -- keep track of whether the module is module_ins local script_data = {} script_data.metadata = { - name = "ultrahdr", + name = _("UltraHDR"), purpose = _("generate UltraHDR images"), author = "Krzysztof Kotowicz" } -local PS = dt.configuration.running_os == "windows" and "\\" or "/" -local ENCODING_VARIANT_SDR_AND_GAINMAP = 1 -local ENCODING_VARIANT_SDR_AND_HDR = 2 -local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3 -local ENCODING_VARIANT_HDR_ONLY = 4 +local PS = dt.configuration.running_os == "windows" and "\\" or "/" -local SELECTION_TYPE_ONE_STACK = 1 -local SELECTION_TYPE_GROUP_BY_FNAME = 2 +local ENCODING_VARIANT_SDR_AND_GAINMAP = 1 +local ENCODING_VARIANT_SDR_AND_HDR = 2 +local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3 +local ENCODING_VARIANT_HDR_ONLY = 4 + +local SELECTION_TYPE_ONE_STACK = 1 +local SELECTION_TYPE_GROUP_BY_FNAME = 2 -- Values are defined in darktable/src/common/colorspaces.h -local DT_COLORSPACE_PQ_P3 = 24 -local DT_COLORSPACE_DISPLAY_P3 = 26 +local DT_COLORSPACE_PQ_P3 = 24 +local DT_COLORSPACE_DISPLAY_P3 = 26 -- 1-based position of a colorspace in export profile combobox. -local COLORSPACE_TO_GUI_ACTION = { +local COLORSPACE_TO_GUI_ACTION = { [DT_COLORSPACE_PQ_P3] = 9, [DT_COLORSPACE_DISPLAY_P3] = 11 } -local UI_SLEEP_MS = 50 -- How many ms to sleep after UI action. +local UI_SLEEP_MS = 50 -- How many ms to sleep after UI action. local function set_log_level(level) local old_log_level = log.log_level() @@ -179,7 +178,7 @@ local function save_preferences() end local function default_to(value, default) - if value == 0 then + if value == 0 or value == "" then return default end return value @@ -193,7 +192,8 @@ local function load_preferences() GUI.optionwidgets.selection_type_combo.selected = math.max( dt.preferences.read(namespace, "selection_type", "integer"), SELECTION_TYPE_ONE_STACK) - GUI.optionwidgets.output_filepath_widget.text = dt.preferences.read(namespace, "output_filepath_pattern", "string") + GUI.optionwidgets.output_filepath_widget.text = default_to(dt.preferences.read(namespace, "output_filepath_pattern", "string"), + "$(FILE_FOLDER)/$(FILE_NAME)_ultrahdr") GUI.optionwidgets.overwrite_on_conflict.value = dt.preferences.read(namespace, "overwrite_on_conflict", "bool") GUI.optionwidgets.import_to_darktable.value = dt.preferences.read(namespace, "import_to_darktable", "bool") GUI.optionwidgets.copy_exif.value = dt.preferences.read(namespace, "copy_exif", "bool") @@ -247,11 +247,11 @@ local function set_combobox(path, instance, config_name, new_config_value) dt.gui.action(path, 0, "selection", "next", 1.0) dt.control.sleep(UI_SLEEP_MS) if dt.preferences.read("darktable", config_name, "integer") == new_config_value then - log.msg(log.debug, string.format(_("Changed %s from %d to %d"), config_name, pref, new_config_value)) + log.msg(log.debug, string.format("Changed %s from %d to %d", config_name, pref, new_config_value)) return pref end end - log.msg(log.error, string.format(_("Could not change %s from %d to %d"), config_name, pref, new_config_value)) + log.msg(log.error, string.format("Could not change %s from %d to %d", config_name, pref, new_config_value)) restore_log_level(old_log_level) end @@ -494,7 +494,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total return cleanup(), errors end end - log.msg(log.debug, string.format(_("Exported files: %s, %s"), sdr, gainmap)) + log.msg(log.debug, string.format("Exported files: %s, %s", sdr, gainmap)) update_job_progress() -- Strip EXIFs table.insert(remove_files, sdr .. ".noexif") @@ -721,9 +721,8 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total end cleanup() update_job_progress() - local msg = string.format(_("Generated %s."), df.get_filename(output_file)) - log.msg(log.info, msg) - dt.print(msg) + log.msg(log.info, string.format("Generated %s.", df.get_filename(output_file))) + dt.print(string.format(_("Generated %s."), df.get_filename(output_file))) restore_log_level(old_log_level) return true, nil end @@ -769,9 +768,8 @@ local function main() job.valid = false end - msg = string.format(_("Generated %d UltraHDR image(s)."), count) - log.msg(log.info, msg) - dt.print(msg) + log.msg(log.info, string.format("Generated %d UltraHDR image(s).", count)) + dt.print(string.format(_("Generated %d UltraHDR image(s)."), count)) restore_log_level(old_log_level) end