@@ -75,7 +75,8 @@ local GUI = {
75
75
metadata_label = {},
76
76
metadata_box = {},
77
77
edit_executables_button = {},
78
- executable_path_widget = {}
78
+ executable_path_widget = {},
79
+ quality_widget = {}
79
80
},
80
81
options = {},
81
82
run = {}
@@ -101,6 +102,10 @@ local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3
101
102
local SELECTION_TYPE_ONE_STACK = 1
102
103
local SELECTION_TYPE_GROUP_BY_FNAME = 2
103
104
105
+ -- Values are defined in darktable/src/common/colorspaces.h
106
+ local DT_COLORSPACE_PQ_P3 = 24
107
+ local DT_COLORSPACE_DISPLAY_P3 = 26
108
+
104
109
local function generate_metadata_file (settings )
105
110
local metadata_file_fmt = [[ --maxContentBoost %f
106
111
--minContentBoost %f
@@ -138,6 +143,7 @@ local function save_preferences()
138
143
dt .preferences .write (namespace , " hdr_capacity_min" , " float" , GUI .optionwidgets .hdr_capacity_min .value )
139
144
dt .preferences .write (namespace , " hdr_capacity_max" , " float" , GUI .optionwidgets .hdr_capacity_max .value )
140
145
end
146
+ dt .preferences .write (namespace , " quality" , " integer" , GUI .optionwidgets .quality_widget .value )
141
147
end
142
148
143
149
local function default_to (value , default )
@@ -169,6 +175,30 @@ local function load_preferences()
169
175
1.0 )
170
176
GUI .optionwidgets .hdr_capacity_max .value = default_to (dt .preferences .read (namespace , " hdr_capacity_max" , " float" ),
171
177
6.0 )
178
+ GUI .optionwidgets .quality_widget .value = default_to (dt .preferences .read (namespace , " quality" , " integer" ), 95 )
179
+ end
180
+
181
+ -- Changes the combobox selection blindly until a paired config value is set.
182
+ -- Workaround for https://github.com/darktable-org/lua-scripts/issues/522
183
+ local function set_combobox (path , instance , config_name , new_config_value )
184
+
185
+ local pref = dt .preferences .read (" darktable" , config_name , " integer" )
186
+ if pref == new_config_value then
187
+ return new_config_value
188
+ end
189
+
190
+ dt .gui .action (path , 0 , " selection" , " first" , 1.0 )
191
+ local limit , i = 30 , 0 -- in case there is no matching config value in the first n entries of a combobox.
192
+ while i < limit do
193
+ i = i + 1
194
+ dt .gui .action (path , 0 , " selection" , " next" , 1.0 )
195
+ dt .control .sleep (10 )
196
+ if dt .preferences .read (" darktable" , config_name , " integer" ) == new_config_value then
197
+ log .msg (log .debug , string.format (_ (" Changed %s from %d to %d" ), config_name , pref , new_config_value ))
198
+ return pref
199
+ end
200
+ end
201
+ log .msg (log .error , string.format (_ (" Could not change %s from %d to %d" ), config_name , pref , new_config_value ))
172
202
end
173
203
174
204
local function assert_settings_correct (encoding_variant )
@@ -189,6 +219,7 @@ local function assert_settings_correct(encoding_variant)
189
219
hdr_capacity_min = GUI .optionwidgets .hdr_capacity_min .value ,
190
220
hdr_capacity_max = GUI .optionwidgets .hdr_capacity_max .value
191
221
},
222
+ quality = GUI .optionwidgets .quality_widget .value ,
192
223
tmpdir = dt .configuration .tmp_dir
193
224
}
194
225
@@ -226,11 +257,10 @@ end
226
257
227
258
local function get_stacks (images , encoding_variant , selection_type )
228
259
local stacks = {}
229
- local extra_image_content_type , extra_image_extension
260
+ local extra_image_content_type
230
261
if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then
231
262
extra_image_content_type = " gainmap"
232
263
elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then
233
- extra_image_extension = " jxl"
234
264
extra_image_content_type = " hdr"
235
265
elseif encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then
236
266
extra_image_content_type = nil
@@ -278,9 +308,6 @@ local function get_stacks(images, encoding_variant, selection_type)
278
308
if extra_image_content_type then
279
309
if not v [" sdr" ] or not v [extra_image_content_type ] then
280
310
stacks [k ] = nil
281
- elseif extra_image_extension and df .get_filetype (v [extra_image_content_type ].filename ) ~=
282
- extra_image_extension then
283
- stacks [k ] = nil
284
311
else
285
312
local sdr_w , sdr_h = get_dimensions (v [" sdr" ])
286
313
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
309
336
local total_substeps
310
337
local substep = 0
311
338
local uhdr
339
+ local errors = {}
312
340
313
341
function update_job_progress ()
314
342
substep = substep + 1
@@ -319,28 +347,56 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
319
347
job .percent = (total_substeps * step + substep ) / (total_steps * total_substeps )
320
348
end
321
349
322
- function copy_or_export_jpg ( src , dest )
323
- if df .get_filetype (src .filename ) == " jpg " and not src .is_altered then
324
- df .file_copy (src .path .. PS .. src .filename , dest )
350
+ function copy_or_export ( src_image , dest , format , colorspace , props )
351
+ if df .get_filetype (src_image .filename ) == df . get_filetype ( dest ) and not src_image .is_altered then
352
+ return df .file_copy (src_image .path .. PS .. src_image .filename , dest )
325
353
else
326
- local exporter = dt .new_format (" jpeg" )
327
- exporter .quality = 95
328
- exporter :write_image (src , dest )
354
+ local prev = set_combobox (" lib/export/profile" , 0 , " plugins/lighttable/export/icctype" , colorspace )
355
+ if not prev then
356
+ return false
357
+ end
358
+ local exporter = dt .new_format (format )
359
+ for k , v in pairs (props ) do
360
+ exporter [k ] = v
361
+ end
362
+ local ok = not exporter :write_image (src_image , dest )
363
+ if prev then
364
+ set_combobox (" lib/export/profile" , 0 , " plugins/lighttable/export/icctype" , prev )
365
+ end
366
+ return ok
329
367
end
368
+ return true
330
369
end
331
370
332
371
if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then
333
372
total_substeps = 6
373
+ local ok
334
374
-- Export/copy both SDR and gainmap to JPEGs
335
375
local sdr = df .create_unique_filename (settings .tmpdir .. PS .. df .chop_filetype (images [" sdr" ].filename ) ..
336
376
" .jpg" )
337
- copy_or_export_jpg (images [" sdr" ], sdr )
377
+ ok = copy_or_export (images [" sdr" ], sdr , " jpeg" , DT_COLORSPACE_DISPLAY_P3 , {
378
+ quality = settings .quality
379
+ })
380
+ if not ok then
381
+ os.remove (sdr )
382
+ table.insert (errors , string.format (_ (" Error exporting %s to %s" ), images [" sdr" ].filename , " jpeg" ))
383
+ return false , errors
384
+ end
385
+
338
386
local gainmap
339
387
if encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then -- SDR is also a gainmap
340
388
gainmap = sdr
341
389
else
342
390
gainmap = df .create_unique_filename (settings .tmpdir .. PS .. images [" gainmap" ].filename .. " _gainmap.jpg" )
343
- copy_or_export_jpg (images [" gainmap" ], gainmap )
391
+ ok = copy_or_export (images [" gainmap" ], gainmap , " jpeg" , DT_COLORSPACE_DISPLAY_P3 , {
392
+ quality = settings .quality
393
+ })
394
+ if not ok then
395
+ os.remove (sdr )
396
+ os.remove (sdr )
397
+ table.insert (errors , string.format (_ (" Error exporting %s to %s" ), images [" gainmap" ].filename , " jpeg" ))
398
+ return false , errors
399
+ end
344
400
end
345
401
log .msg (log .debug , string.format (_ (" Exported files: %s, %s" ), sdr , gainmap ))
346
402
update_job_progress ()
@@ -377,30 +433,54 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
377
433
end
378
434
update_job_progress ()
379
435
elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then
380
- total_substeps = 5
436
+ local ok
437
+ total_substeps = 6
381
438
-- https://discuss.pixls.us/t/manual-creation-of-ultrahdr-images/45004/20
382
- -- Step 1: Export SDR to PNG (HDR is already a JPEG-XL)
383
- local exporter = dt .new_format (" png" )
384
- exporter .bpp = 8
439
+ -- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3
440
+ local hdr = df .create_unique_filename (settings .tmpdir .. PS .. df .chop_filetype (images [" hdr" ].filename ) ..
441
+ " .jxl" )
442
+ ok = copy_or_export (images [" hdr" ], hdr , " jpegxl" , DT_COLORSPACE_PQ_P3 , {
443
+ bpp = 10 ,
444
+ quality = 100 -- lossless
445
+ })
446
+ if not ok then
447
+ os.remove (hdr )
448
+ table.insert (errors , string.format (_ (" Error exporting %s to %s" ), images [" hdr" ].filename , " jxl" ))
449
+ return false , errors
450
+ end
451
+ update_job_progress ()
452
+ -- Step 2: Export SDR to PNG
385
453
local sdr = df .create_unique_filename (settings .tmpdir .. PS .. df .chop_filetype (images [" sdr" ].filename ) ..
386
454
" .png" )
387
- exporter :write_image (images [" sdr" ], sdr )
455
+ ok = copy_or_export (images [" sdr" ], sdr , " png" , DT_COLORSPACE_DISPLAY_P3 , {
456
+ bpp = 8
457
+ })
458
+ if not ok then
459
+ os.remove (hdr )
460
+ os.remove (sdr )
461
+ table.insert (errors , string.format (_ (" Error exporting %s to %s" ), images [" sdr" ].filename , " png" ))
462
+ return false , errors
463
+ end
388
464
uhdr = df .chop_filetype (sdr ) .. " _ultrahdr.jpg"
389
465
390
466
update_job_progress ()
391
- local extra = df .create_unique_filename (settings .tmpdir .. PS .. images [" hdr" ].filename .. " .raw" )
392
-
393
- -- Step 3: Generate libultrahdr RAW images
394
- execute_cmd (settings .bin .ffmpeg .. " -i " .. df .sanitize_filename (sdr ) .. " -pix_fmt rgba -f rawvideo " ..
467
+ -- Step 3: Generate libultrahdr RAW images
468
+ local sdr_w , sdr_h = get_dimensions (images [" sdr" ])
469
+ local resize_cmd = " "
470
+ if sdr_h % 2 + sdr_w % 2 > 0 then -- needs resizing to even dimensions.
471
+ resize_cmd = string.format (" -vf 'crop=%d:%d:0:0' " , sdr_w - sdr_w % 2 , sdr_h - sdr_h % 2 )
472
+ end
473
+
474
+ execute_cmd (settings .bin .ffmpeg .. " -i " .. df .sanitize_filename (sdr ) .. resize_cmd .. " -pix_fmt rgba -f rawvideo " ..
395
475
df .sanitize_filename (sdr .. " .raw" ))
396
- execute_cmd (settings .bin .ffmpeg .. " -i " ..
397
- df .sanitize_filename (images [" hdr" ].path .. PS .. images [" hdr" ].filename ) ..
398
- " -pix_fmt p010le -f rawvideo " .. df .sanitize_filename (extra ))
476
+ execute_cmd (settings .bin .ffmpeg .. " -i " .. df .sanitize_filename (hdr ) .. resize_cmd .. " -pix_fmt p010le -f rawvideo " ..
477
+ df .sanitize_filename (hdr .. " .raw" ))
399
478
update_job_progress ()
400
- local sdr_w , sdr_h = get_dimensions (images [" sdr" ])
401
479
execute_cmd (settings .bin .ultrahdr_app .. " -m 0 -y " .. df .sanitize_filename (sdr .. " .raw" ) .. " -p " ..
402
- 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 " ..
403
- tostring (sdr_w ) .. " -h " .. tostring (sdr_h ) .. " -z " .. df .sanitize_filename (uhdr ))
480
+ df .sanitize_filename (hdr .. " .raw" ) ..
481
+ string.format (" -a 0 -b 3 -c 1 -C 1 -t 2 -M 1 -s 1 -q %d -Q %d -D 1 " , settings .quality ,
482
+ settings .quality ) .. " -w " .. tostring (sdr_w - sdr_w % 2 ) .. " -h " .. tostring (sdr_h - sdr_h % 2 ) .. " -z " ..
483
+ df .sanitize_filename (uhdr ))
404
484
update_job_progress ()
405
485
if settings .copy_exif then
406
486
-- 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
409
489
df .sanitize_filename (uhdr ) .. " -overwrite_original -preserve" )
410
490
end
411
491
-- Cleanup
492
+ os.remove (hdr )
412
493
os.remove (sdr )
494
+ os.remove (hdr .. " .raw" )
413
495
os.remove (sdr .. " .raw" )
414
- os.remove (extra )
415
496
update_job_progress ()
416
497
end
417
498
@@ -433,12 +514,10 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
433
514
log .msg (log .info , msg )
434
515
dt .print (msg )
435
516
update_job_progress ()
517
+ return true , nil
436
518
end
437
519
438
520
local function main ()
439
- local saved_log_level = log .log_level ()
440
- log .log_level (log .info )
441
-
442
521
save_preferences ()
443
522
444
523
local selection_type = GUI .optionwidgets .selection_type_combo .selected
@@ -447,21 +526,25 @@ local function main()
447
526
448
527
local settings , errors = assert_settings_correct (encoding_variant )
449
528
if not settings then
450
- dt .print (string.format (_ (" Export settings are incorrect, exiting:\n\n %s" ), table.concat (errors , " \n " )))
451
- log .log_level (saved_log_level )
529
+ dt .print (string.format (_ (" Export settings are incorrect, exiting:\n\n - %s" ), table.concat (errors , " \n - " )))
452
530
return
453
531
end
454
532
455
533
local stacks , stack_count = get_stacks (dt .gui .selection (), encoding_variant , selection_type )
456
534
dt .print (string.format (_ (" Detected %d image stack(s)" ), stack_count ))
457
535
if stack_count == 0 then
458
- log .log_level (saved_log_level )
459
536
return
460
537
end
461
538
job = dt .gui .create_job (_ (" Generating UltraHDR images" ), true , stop_job )
462
539
local count = 0
540
+ local msg
463
541
for i , v in pairs (stacks ) do
464
- generate_ultrahdr (encoding_variant , v , settings , count , stack_count )
542
+ local ok , errors = generate_ultrahdr (encoding_variant , v , settings , count , stack_count )
543
+ if not ok then
544
+ dt .print (string.format (_ (" Errors generating images:\n\n - %s" ), table.concat (errors , " \n - " )))
545
+ job .valid = false
546
+ return
547
+ end
465
548
count = count + 1
466
549
-- sleep for a short moment to give stop_job callback function a chance to run
467
550
dt .control .sleep (10 )
@@ -471,10 +554,9 @@ local function main()
471
554
job .valid = false
472
555
end
473
556
474
- local msg = string.format (_ (" Generated %d UltraHDR image(s)." ), count )
557
+ msg = string.format (_ (" Generated %d UltraHDR image(s)." ), count )
475
558
log .msg (log .info , msg )
476
559
dt .print (msg )
477
- log .log_level (saved_log_level )
478
560
end
479
561
480
562
GUI .optionwidgets .settings_label = dt .new_widget (" section_label" ) {
@@ -591,12 +673,12 @@ GUI.optionwidgets.encoding_variant_combo = dt.new_widget("combobox") {
591
673
This will determine the method used to generate UltraHDR.
592
674
593
675
- %s: SDR image paired with a gain map image.
594
- - %s: SDR image paired with a JPEG-XL HDR image (10-bit, 'PQ P3 RGB' profile recommended) .
595
- - %s: SDR images only . Gainmaps will be copies of SDR images (the simplest option) .
676
+ - %s: SDR image paired with an HDR image.
677
+ - %s: Each stack consists of a single SDR image . Gainmaps will be copies of SDR images.
596
678
597
679
By default, the first image in a stack is treated as SDR, and the second one is a gainmap/HDR.
598
- You can force the image into a specific stack slot by attaching "hdr" / "gainmap" tags to them .
599
- ]] ), _ (" SDR + gainmap" ), _ (" SDR + JPEG-XL HDR" ), _ (" SDR only" )),
680
+ You can force the image into a specific stack slot by attaching "hdr" / "gainmap" tags to it .
681
+ ]] ), _ (" SDR + gainmap" ), _ (" SDR + HDR" ), _ (" SDR only" )),
600
682
selected = 0 ,
601
683
changed_callback = function (self )
602
684
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
607
689
end
608
690
end ,
609
691
_ (" SDR + gainmap" ), -- ENCODING_VARIANT_SDR_AND_GAINMAP
610
- _ (" SDR + JPEG-XL HDR" ), -- ENCODING_VARIANT_SDR_AND_HDR
692
+ _ (" SDR + HDR" ), -- ENCODING_VARIANT_SDR_AND_HDR
611
693
_ (" SDR only" ) -- ENCODING_VARIANT_SDR_AUTO_GAINMAP
612
694
}
613
695
@@ -627,10 +709,25 @@ As an added precaution, each image in a stack needs to have the same dimensions.
627
709
_ (" multiple stacks (use filename)" ) -- SELECTION_TYPE_GROUP_BY_FNAME
628
710
}
629
711
712
+ GUI .optionwidgets .quality_widget = dt .new_widget (" slider" ) {
713
+ label = _ (' Quality' ),
714
+ tooltip = _ (' Quality of the output UltraHDR JPEG file' ),
715
+ hard_min = 0 ,
716
+ hard_max = 100 ,
717
+ soft_min = 0 ,
718
+ soft_max = 100 ,
719
+ step = 1 ,
720
+ digits = 0 ,
721
+ reset_callback = function (self )
722
+ self .value = 95
723
+ end
724
+ }
725
+
630
726
GUI .optionwidgets .encoding_settings_box = dt .new_widget (" box" ) {
631
727
orientation = " vertical" ,
632
728
GUI .optionwidgets .selection_type_combo ,
633
729
GUI .optionwidgets .encoding_variant_combo ,
730
+ GUI .optionwidgets .quality_widget ,
634
731
GUI .optionwidgets .metadata_box
635
732
}
636
733
@@ -656,10 +753,7 @@ GUI.options = dt.new_widget("box") {
656
753
657
754
GUI .run = dt .new_widget (" button" ) {
658
755
label = _ (" Generate UltraHDR" ),
659
- tooltip = _ ([[ Generate UltraHDR image(s) from selection
660
-
661
- 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)
662
- ]] ),
756
+ tooltip = _ (" Generate UltraHDR image(s) from selection" ),
663
757
clicked_callback = main
664
758
}
665
759
0 commit comments