@@ -79,6 +79,7 @@ local GUI = {
7979 executable_path_widget = {},
8080 quality_widget = {},
8181 gainmap_downsampling_widget = {},
82+ target_display_peak_nits_widget = {}
8283 },
8384 options = {},
8485 run = {}
@@ -100,6 +101,7 @@ local PS = dt.configuration.running_os == "windows" and "\\" or "/"
100101local ENCODING_VARIANT_SDR_AND_GAINMAP = 1
101102local ENCODING_VARIANT_SDR_AND_HDR = 2
102103local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3
104+ local ENCODING_VARIANT_HDR_ONLY = 4
103105
104106local SELECTION_TYPE_ONE_STACK = 1
105107local SELECTION_TYPE_GROUP_BY_FNAME = 2
@@ -146,7 +148,11 @@ local function save_preferences()
146148 dt .preferences .write (namespace , " hdr_capacity_max" , " float" , GUI .optionwidgets .hdr_capacity_max .value )
147149 end
148150 dt .preferences .write (namespace , " quality" , " integer" , GUI .optionwidgets .quality_widget .value )
149- dt .preferences .write (namespace , " gainmap_downsampling" , " integer" , GUI .optionwidgets .gainmap_downsampling_widget .value )
151+ dt .preferences .write (namespace , " gainmap_downsampling" , " integer" ,
152+ GUI .optionwidgets .gainmap_downsampling_widget .value )
153+ dt .preferences .write (namespace , " target_display_peak_nits" , " integer" ,
154+ (GUI .optionwidgets .target_display_peak_nits_widget .value + 0.5 )// 1 )
155+
150156end
151157
152158local function default_to (value , default )
@@ -179,7 +185,10 @@ local function load_preferences()
179185 GUI .optionwidgets .hdr_capacity_max .value = default_to (dt .preferences .read (namespace , " hdr_capacity_max" , " float" ),
180186 6.0 )
181187 GUI .optionwidgets .quality_widget .value = default_to (dt .preferences .read (namespace , " quality" , " integer" ), 95 )
182- GUI .optionwidgets .gainmap_downsampling_widget .value = default_to (dt .preferences .read (namespace , " gainmap_downsampling" , " integer" ), 0 )
188+ GUI .optionwidgets .target_display_peak_nits_widget .value = default_to (
189+ dt .preferences .read (namespace , " target_display_peak_nits" , " integer" ), 10000 )
190+ GUI .optionwidgets .gainmap_downsampling_widget .value = default_to (
191+ dt .preferences .read (namespace , " gainmap_downsampling" , " integer" ), 0 )
183192end
184193
185194-- 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)
192201 end
193202
194203 dt .gui .action (path , 0 , " selection" , " first" , 1.0 )
204+ dt .control .sleep (50 )
195205 local limit , i = 30 , 0 -- in case there is no matching config value in the first n entries of a combobox.
196206 while i < limit do
197207 i = i + 1
198208 dt .gui .action (path , 0 , " selection" , " next" , 1.0 )
199- dt .control .sleep (10 )
209+ dt .control .sleep (50 )
200210 if dt .preferences .read (" darktable" , config_name , " integer" ) == new_config_value then
201211 log .msg (log .debug , string.format (_ (" Changed %s from %d to %d" ), config_name , pref , new_config_value ))
202212 return pref
@@ -224,7 +234,8 @@ local function assert_settings_correct(encoding_variant)
224234 hdr_capacity_max = GUI .optionwidgets .hdr_capacity_max .value
225235 },
226236 quality = GUI .optionwidgets .quality_widget .value ,
227- downsample = 2 ^ GUI .optionwidgets .gainmap_downsampling_widget .value ,
237+ target_display_peak_nits = (GUI .optionwidgets .target_display_peak_nits_widget .value + 0.5 )// 1 ,
238+ downsample = 2 ^ GUI .optionwidgets .gainmap_downsampling_widget .value ,
228239 tmpdir = dt .configuration .tmp_dir ,
229240 skip_cleanup = false -- keep temporary files around, for debugging.
230241 }
@@ -263,23 +274,27 @@ end
263274
264275local function get_stacks (images , encoding_variant , selection_type )
265276 local stacks = {}
266- local extra_image_content_type
277+ local primary = " sdr"
278+ local extra
267279 if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then
268- extra_image_content_type = " gainmap"
280+ extra = " gainmap"
269281 elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then
270- extra_image_content_type = " hdr"
282+ extra = " hdr"
271283 elseif encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then
272- extra_image_content_type = nil
284+ extra = nil
285+ elseif encoding_variant == ENCODING_VARIANT_HDR_ONLY then
286+ extra = nil
287+ primary = " hdr"
273288 end
274289
275290 local tags = nil
276- -- Group images into (sdr [,extra]) stacks
277- -- 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
291+ -- Group images into (primary [,extra]) stacks
292+ -- 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
278293 for k , v in pairs (images ) do
279294 local is_extra = false
280295 tags = dt .tags .get_tags (v )
281296 for ignore , tag in pairs (tags ) do
282- if extra_image_content_type and tag .name == extra_image_content_type then
297+ if extra and tag .name == extra then
283298 is_extra = true
284299 end
285300 end
@@ -296,27 +311,27 @@ local function get_stacks(images, encoding_variant, selection_type)
296311 if stacks [key ] == nil then
297312 stacks [key ] = {}
298313 end
299- if extra_image_content_type and (is_extra or stacks [key ][" sdr " ]) then
314+ if extra and (is_extra or stacks [key ][primary ]) then
300315 -- Don't overwrite existing entries
301- if not stacks [key ][extra_image_content_type ] then
302- stacks [key ][extra_image_content_type ] = v
316+ if not stacks [key ][extra ] then
317+ stacks [key ][extra ] = v
303318 end
304319 elseif not is_extra then
305320 -- Don't overwrite existing entries
306- if not stacks [key ][" sdr " ] then
307- stacks [key ][" sdr " ] = v
321+ if not stacks [key ][primary ] then
322+ stacks [key ][primary ] = v
308323 end
309324 end
310325 end
311326 -- remove invalid stacks
312327 local count = 0
313328 for k , v in pairs (stacks ) do
314- if extra_image_content_type then
315- if not v [" sdr " ] or not v [extra_image_content_type ] then
329+ if extra then
330+ if not v [primary ] or not v [extra ] then
316331 stacks [k ] = nil
317332 else
318- local sdr_w , sdr_h = get_dimensions (v [" sdr " ])
319- local extra_w , extra_h = get_dimensions (v [extra_image_content_type ])
333+ local sdr_w , sdr_h = get_dimensions (v [primary ])
334+ local extra_w , extra_h = get_dimensions (v [extra ])
320335 if (sdr_w ~= extra_w ) or (sdr_h ~= extra_h ) then
321336 stacks [k ] = nil
322337 end
346361local function generate_ultrahdr (encoding_variant , images , settings , step , total_steps )
347362 local total_substeps
348363 local substep = 0
364+ local best_source_image
349365 local uhdr
350366 local errors = {}
351367 local remove_files = {}
@@ -408,6 +424,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
408424
409425 if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then
410426 total_substeps = 5
427+ best_source_image = images [" sdr" ]
411428 -- Export/copy both SDR and gainmap to JPEGs
412429 local sdr = df .create_unique_filename (settings .tmpdir .. PS .. df .chop_filetype (images [" sdr" ].filename ) ..
413430 " .jpg" )
@@ -456,9 +473,13 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
456473 -- Merge files
457474 uhdr = df .chop_filetype (sdr ) .. " _ultrahdr.jpg"
458475 table.insert (remove_files , uhdr )
459- cmd = settings .bin .ultrahdr_app .. " -m 0 -i " .. df .sanitize_filename (sdr .. " .noexif" ) .. " -g " ..
460- df .sanitize_filename (gainmap ) .. " -f " .. df .sanitize_filename (metadata_file ) .. " -z " ..
461- df .sanitize_filename (uhdr )
476+ cmd = settings .bin .ultrahdr_app ..
477+ string.format (" -m 0 -i %s -g %s -L %d -f %s -z %s" , df .sanitize_filename (sdr .. " .noexif" ), -- -i
478+ df .sanitize_filename (gainmap ), -- -g
479+ settings .target_display_peak_nits , -- -L
480+ df .sanitize_filename (metadata_file ), -- -f
481+ df .sanitize_filename (uhdr ) -- -z
482+ )
462483 if not execute_cmd (cmd , string.format (_ (" Error merging UltraHDR to %s" ), uhdr )) then
463484 return cleanup (), errors
464485 end
@@ -476,6 +497,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
476497 update_job_progress ()
477498 elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then
478499 total_substeps = 6
500+ best_source_image = images [" sdr" ]
479501 -- https://discuss.pixls.us/t/manual-creation-of-ultrahdr-images/45004/20
480502 -- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3
481503 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
528550 end
529551 -- sanity check for file sizes (sometimes dt exports different size images if the files were never opened in darktable view)
530552 if file_size (sdr_raw ) ~= size_in_px * 4 or file_size (hdr_raw ) ~= size_in_px * 3 then
531- 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 ))
553+ table.insert (errors ,
554+ string.format (
555+ _ (" Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first." ),
556+ images [" sdr" ].filename , sdr_w , sdr_h ))
532557 return cleanup (), errors
533558 end
534559 update_job_progress ()
535- cmd = settings .bin .ultrahdr_app .. " -m 0 -y " .. df .sanitize_filename (sdr_raw ) .. " -p " ..
536- df .sanitize_filename (hdr_raw ) ..
537- string.format (" -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -s 1 -q %d -Q %d -D 1 " , settings .quality ,
538- settings .quality ) .. string.format (" -s %d " , settings .downsample ) .. " -w " .. tostring (sdr_w - sdr_w % 2 ) .. " -h " .. tostring (sdr_h - sdr_h % 2 ) ..
539- " -z " .. df .sanitize_filename (uhdr )
560+ cmd = settings .bin .ultrahdr_app ..
561+ string.format (
562+ " -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" ,
563+ df .sanitize_filename (sdr_raw ), -- -y
564+ df .sanitize_filename (hdr_raw ), -- -p
565+ settings .quality , -- -q
566+ settings .quality , -- -Q
567+ settings .target_display_peak_nits , -- -L
568+ settings .downsample , -- -s
569+ sdr_w - sdr_w % 2 , -- w
570+ sdr_h - sdr_h % 2 , -- h
571+ df .sanitize_filename (uhdr ) -- z
572+ )
540573 if not execute_cmd (cmd , string.format (_ (" Error merging %s" ), uhdr )) then
541574 return cleanup (), errors
542575 end
@@ -551,13 +584,82 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
551584 end
552585 end
553586 update_job_progress ()
587+ elseif encoding_variant == ENCODING_VARIANT_HDR_ONLY then
588+ total_substeps = 5
589+ best_source_image = images [" hdr" ]
590+ -- TODO: Check if exporting to JXL would be ok too.
591+ -- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3
592+ local hdr = df .create_unique_filename (settings .tmpdir .. PS .. df .chop_filetype (images [" hdr" ].filename ) ..
593+ " .jxl" )
594+ table.insert (remove_files , hdr )
595+ ok = copy_or_export (images [" hdr" ], hdr , " jpegxl" , DT_COLORSPACE_PQ_P3 , {
596+ bpp = 10 ,
597+ quality = 100 , -- lossless
598+ effort = 1 -- we don't care about the size, the file is temporary.
599+ })
600+ if not ok then
601+ table.insert (errors , string.format (_ (" Error exporting %s to %s" ), images [" hdr" ].filename , " jxl" ))
602+ return cleanup (), errors
603+ end
604+ update_job_progress ()
605+ -- Step 1: Generate raw HDR image
606+ local hdr_raw = df .create_unique_filename (settings .tmpdir .. PS .. df .chop_filetype (images [" hdr" ].filename ) ..
607+ " .raw" )
608+ table.insert (remove_files , hdr_raw )
609+ local hdr_w , hdr_h = get_dimensions (images [" hdr" ])
610+ local resize_cmd = " "
611+ if hdr_h % 2 + hdr_w % 2 > 0 then -- needs resizing to even dimensions.
612+ resize_cmd = string.format (" -vf 'crop=%d:%d:0:0' " , hdr_w - hdr_w % 2 , hdr_h - hdr_h % 2 )
613+ end
614+ local size_in_px = (hdr_w - hdr_w % 2 ) * (hdr_h - hdr_h % 2 )
615+ cmd = settings .bin .ffmpeg .. " -i " .. df .sanitize_filename (hdr ) .. resize_cmd ..
616+ " -pix_fmt p010le -f rawvideo " .. df .sanitize_filename (hdr_raw )
617+ if not execute_cmd (cmd , string.format (_ (" Error generating %s" ), hdr_raw )) then
618+ return cleanup (), errors
619+ end
620+ if file_size (hdr_raw ) ~= size_in_px * 3 then
621+ table.insert (errors ,
622+ string.format (
623+ _ (" Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first." ),
624+ images [" hdr" ].filename , hdr_w , hdr_h ))
625+ return cleanup (), errors
626+ end
627+ update_job_progress ()
628+ uhdr = df .chop_filetype (hdr_raw ) .. " _ultrahdr.jpg"
629+ table.insert (remove_files , uhdr )
630+ cmd = settings .bin .ultrahdr_app ..
631+ string.format (
632+ " -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" ,
633+ df .sanitize_filename (hdr_raw ), -- -p
634+ settings .quality , -- -q
635+ settings .quality , -- -Q
636+ settings .target_display_peak_nits , -- -L
637+ settings .downsample , -- s
638+ hdr_w - hdr_w % 2 , -- -w
639+ hdr_h - hdr_h % 2 , -- -h
640+ df .sanitize_filename (uhdr ) -- -z
641+ )
642+ if not execute_cmd (cmd , string.format (_ (" Error merging %s" ), uhdr )) then
643+ return cleanup (), errors
644+ end
645+ update_job_progress ()
646+ if settings .copy_exif then
647+ -- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all).
648+ -- This might hapen e.g. when the source files are Adobe gainmap HDRs.
649+ cmd = settings .bin .exiftool .. " -tagsfromfile " .. df .sanitize_filename (hdr ) .. " -exif " ..
650+ df .sanitize_filename (uhdr ) .. " -overwrite_original -preserve"
651+ if not execute_cmd (cmd , string.format (_ (" Error adding EXIF to %s" ), uhdr )) then
652+ return cleanup (), errors
653+ end
654+ end
655+ update_job_progress ()
554656 end
555657
556- local output_dir = settings .use_original_dir and images [ " sdr " ] .path or settings .output
658+ local output_dir = settings .use_original_dir and best_source_image .path or settings .output
557659 local output_file = df .create_unique_filename (output_dir .. PS .. df .get_filename (uhdr ))
558660 ok = df .file_move (uhdr , output_file )
559661 if not ok then
560- table.insert (errors , string.format (_ (" Error generating UltraHDR for %s" ), images [ " sdr " ] .filename ))
662+ table.insert (errors , string.format (_ (" Error generating UltraHDR for %s" ), best_source_image .filename ))
561663 return cleanup (), errors
562664 end
563665 if settings .import_to_darktable then
@@ -593,7 +695,9 @@ local function main()
593695
594696 local stacks , stack_count = get_stacks (dt .gui .selection (), encoding_variant , selection_type )
595697 if stack_count == 0 then
596- dt .print (string.format (_ (" No image stacks detected.\n\n Make sure that the image pairs have the same widths and heights." ), stack_count ))
698+ dt .print (string.format (_ (
699+ " No image stacks detected.\n\n Make sure that the image pairs have the same widths and heights." ),
700+ stack_count ))
597701 return
598702 end
599703 dt .print (string.format (_ (" Detected %d image stack(s)" ), stack_count ))
@@ -737,10 +841,11 @@ This will determine the method used to generate UltraHDR.
737841- %s: SDR image paired with a gain map image.
738842- %s: SDR image paired with an HDR image.
739843- %s: Each stack consists of a single SDR image. Gain maps will be copies of SDR images.
844+ - %s: Each stack consists of a single HDR image. HDR will be tone mapped to SDR.
740845
741846By default, the first image in a stack is treated as SDR, and the second one is a gain map/HDR.
742847You can force the image into a specific stack slot by attaching "hdr" / "gainmap" tags to it.
743- ]] ), _ (" SDR + gain map" ), _ (" SDR + HDR" ), _ (" SDR only" )),
848+ ]] ), _ (" SDR + gain map" ), _ (" SDR + HDR" ), _ (" SDR only" ), _ ( " HDR only " ) ),
744849 selected = 0 ,
745850 changed_callback = function (self )
746851 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
754859 end ,
755860 _ (" SDR + gain map" ), -- ENCODING_VARIANT_SDR_AND_GAINMAP
756861 _ (" SDR + HDR" ), -- ENCODING_VARIANT_SDR_AND_HDR
757- _ (" SDR only" ) -- ENCODING_VARIANT_SDR_AUTO_GAINMAP
862+ _ (" SDR only" ), -- ENCODING_VARIANT_SDR_AUTO_GAINMAP
863+ _ (" HDR only" ) -- ENCODING_VARIANT_HDR_ONLY
758864}
759865
760866GUI .optionwidgets .selection_type_combo = dt .new_widget (" combobox" ) {
@@ -787,9 +893,24 @@ GUI.optionwidgets.quality_widget = dt.new_widget("slider") {
787893 end
788894}
789895
896+ GUI .optionwidgets .target_display_peak_nits_widget = dt .new_widget (" slider" ) {
897+ label = _ (' target display peak brightness (nits)' ),
898+ tooltip = _ (' Peak brightness of target display in nits (defaults to 10000)' ),
899+ hard_min = 203 ,
900+ hard_max = 10000 ,
901+ soft_min = 1000 ,
902+ soft_max = 10000 ,
903+ step = 10 ,
904+ digits = 0 ,
905+ reset_callback = function (self )
906+ self .value = 10000
907+ end
908+ }
909+
790910GUI .optionwidgets .gainmap_downsampling_widget = dt .new_widget (" slider" ) {
791911 label = _ (' gain map downsampling steps' ),
792- tooltip = _ (' Exponent (2^x) of the gain map downsampling factor.\n Downsampling reduces the gain map resolution.\n\n 0 = don\' t downsample the gain map, 7 = maximum downsampling (128x)' ),
912+ tooltip = _ (
913+ ' Exponent (2^x) of the gain map downsampling factor.\n Downsampling reduces the gain map resolution.\n\n 0 = don\' t downsample the gain map, 7 = maximum downsampling (128x)' ),
793914 hard_min = 0 ,
794915 hard_max = 7 ,
795916 soft_min = 0 ,
@@ -807,6 +928,7 @@ GUI.optionwidgets.encoding_settings_box = dt.new_widget("box") {
807928 GUI .optionwidgets .encoding_variant_combo ,
808929 GUI .optionwidgets .quality_widget ,
809930 GUI .optionwidgets .gainmap_downsampling_widget ,
931+ GUI .optionwidgets .target_display_peak_nits_widget ,
810932 GUI .optionwidgets .metadata_box
811933}
812934
0 commit comments