@@ -79,6 +79,7 @@ local GUI = {
79
79
executable_path_widget = {},
80
80
quality_widget = {},
81
81
gainmap_downsampling_widget = {},
82
+ target_display_peak_nits_widget = {}
82
83
},
83
84
options = {},
84
85
run = {}
@@ -100,6 +101,7 @@ local PS = dt.configuration.running_os == "windows" and "\\" or "/"
100
101
local ENCODING_VARIANT_SDR_AND_GAINMAP = 1
101
102
local ENCODING_VARIANT_SDR_AND_HDR = 2
102
103
local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3
104
+ local ENCODING_VARIANT_HDR_ONLY = 4
103
105
104
106
local SELECTION_TYPE_ONE_STACK = 1
105
107
local SELECTION_TYPE_GROUP_BY_FNAME = 2
@@ -146,7 +148,11 @@ local function save_preferences()
146
148
dt .preferences .write (namespace , " hdr_capacity_max" , " float" , GUI .optionwidgets .hdr_capacity_max .value )
147
149
end
148
150
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
+
150
156
end
151
157
152
158
local function default_to (value , default )
@@ -179,7 +185,10 @@ local function load_preferences()
179
185
GUI .optionwidgets .hdr_capacity_max .value = default_to (dt .preferences .read (namespace , " hdr_capacity_max" , " float" ),
180
186
6.0 )
181
187
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 )
183
192
end
184
193
185
194
-- 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)
192
201
end
193
202
194
203
dt .gui .action (path , 0 , " selection" , " first" , 1.0 )
204
+ dt .control .sleep (50 )
195
205
local limit , i = 30 , 0 -- in case there is no matching config value in the first n entries of a combobox.
196
206
while i < limit do
197
207
i = i + 1
198
208
dt .gui .action (path , 0 , " selection" , " next" , 1.0 )
199
- dt .control .sleep (10 )
209
+ dt .control .sleep (50 )
200
210
if dt .preferences .read (" darktable" , config_name , " integer" ) == new_config_value then
201
211
log .msg (log .debug , string.format (_ (" Changed %s from %d to %d" ), config_name , pref , new_config_value ))
202
212
return pref
@@ -224,7 +234,8 @@ local function assert_settings_correct(encoding_variant)
224
234
hdr_capacity_max = GUI .optionwidgets .hdr_capacity_max .value
225
235
},
226
236
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 ,
228
239
tmpdir = dt .configuration .tmp_dir ,
229
240
skip_cleanup = false -- keep temporary files around, for debugging.
230
241
}
@@ -263,23 +274,27 @@ end
263
274
264
275
local function get_stacks (images , encoding_variant , selection_type )
265
276
local stacks = {}
266
- local extra_image_content_type
277
+ local primary = " sdr"
278
+ local extra
267
279
if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then
268
- extra_image_content_type = " gainmap"
280
+ extra = " gainmap"
269
281
elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then
270
- extra_image_content_type = " hdr"
282
+ extra = " hdr"
271
283
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"
273
288
end
274
289
275
290
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
278
293
for k , v in pairs (images ) do
279
294
local is_extra = false
280
295
tags = dt .tags .get_tags (v )
281
296
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
283
298
is_extra = true
284
299
end
285
300
end
@@ -296,27 +311,27 @@ local function get_stacks(images, encoding_variant, selection_type)
296
311
if stacks [key ] == nil then
297
312
stacks [key ] = {}
298
313
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
300
315
-- 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
303
318
end
304
319
elseif not is_extra then
305
320
-- 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
308
323
end
309
324
end
310
325
end
311
326
-- remove invalid stacks
312
327
local count = 0
313
328
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
316
331
stacks [k ] = nil
317
332
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 ])
320
335
if (sdr_w ~= extra_w ) or (sdr_h ~= extra_h ) then
321
336
stacks [k ] = nil
322
337
end
346
361
local function generate_ultrahdr (encoding_variant , images , settings , step , total_steps )
347
362
local total_substeps
348
363
local substep = 0
364
+ local best_source_image
349
365
local uhdr
350
366
local errors = {}
351
367
local remove_files = {}
@@ -408,6 +424,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
408
424
409
425
if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then
410
426
total_substeps = 5
427
+ best_source_image = images [" sdr" ]
411
428
-- Export/copy both SDR and gainmap to JPEGs
412
429
local sdr = df .create_unique_filename (settings .tmpdir .. PS .. df .chop_filetype (images [" sdr" ].filename ) ..
413
430
" .jpg" )
@@ -456,9 +473,13 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
456
473
-- Merge files
457
474
uhdr = df .chop_filetype (sdr ) .. " _ultrahdr.jpg"
458
475
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
+ )
462
483
if not execute_cmd (cmd , string.format (_ (" Error merging UltraHDR to %s" ), uhdr )) then
463
484
return cleanup (), errors
464
485
end
@@ -476,6 +497,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
476
497
update_job_progress ()
477
498
elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then
478
499
total_substeps = 6
500
+ best_source_image = images [" sdr" ]
479
501
-- https://discuss.pixls.us/t/manual-creation-of-ultrahdr-images/45004/20
480
502
-- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3
481
503
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
528
550
end
529
551
-- sanity check for file sizes (sometimes dt exports different size images if the files were never opened in darktable view)
530
552
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 ))
532
557
return cleanup (), errors
533
558
end
534
559
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
+ )
540
573
if not execute_cmd (cmd , string.format (_ (" Error merging %s" ), uhdr )) then
541
574
return cleanup (), errors
542
575
end
@@ -551,13 +584,82 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
551
584
end
552
585
end
553
586
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 ()
554
656
end
555
657
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
557
659
local output_file = df .create_unique_filename (output_dir .. PS .. df .get_filename (uhdr ))
558
660
ok = df .file_move (uhdr , output_file )
559
661
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 ))
561
663
return cleanup (), errors
562
664
end
563
665
if settings .import_to_darktable then
@@ -593,7 +695,9 @@ local function main()
593
695
594
696
local stacks , stack_count = get_stacks (dt .gui .selection (), encoding_variant , selection_type )
595
697
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 ))
597
701
return
598
702
end
599
703
dt .print (string.format (_ (" Detected %d image stack(s)" ), stack_count ))
@@ -737,10 +841,11 @@ This will determine the method used to generate UltraHDR.
737
841
- %s: SDR image paired with a gain map image.
738
842
- %s: SDR image paired with an HDR image.
739
843
- %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.
740
845
741
846
By default, the first image in a stack is treated as SDR, and the second one is a gain map/HDR.
742
847
You 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 " ) ),
744
849
selected = 0 ,
745
850
changed_callback = function (self )
746
851
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
754
859
end ,
755
860
_ (" SDR + gain map" ), -- ENCODING_VARIANT_SDR_AND_GAINMAP
756
861
_ (" 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
758
864
}
759
865
760
866
GUI .optionwidgets .selection_type_combo = dt .new_widget (" combobox" ) {
@@ -787,9 +893,24 @@ GUI.optionwidgets.quality_widget = dt.new_widget("slider") {
787
893
end
788
894
}
789
895
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
+
790
910
GUI .optionwidgets .gainmap_downsampling_widget = dt .new_widget (" slider" ) {
791
911
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)' ),
793
914
hard_min = 0 ,
794
915
hard_max = 7 ,
795
916
soft_min = 0 ,
@@ -807,6 +928,7 @@ GUI.optionwidgets.encoding_settings_box = dt.new_widget("box") {
807
928
GUI .optionwidgets .encoding_variant_combo ,
808
929
GUI .optionwidgets .quality_widget ,
809
930
GUI .optionwidgets .gainmap_downsampling_widget ,
931
+ GUI .optionwidgets .target_display_peak_nits_widget ,
810
932
GUI .optionwidgets .metadata_box
811
933
}
812
934
0 commit comments