diff --git a/ChangeLog.md b/ChangeLog.md index d935ab16..33b27bd2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,78 @@ ## Changes from most recent to oldest +**06 Jun 2024 - wpferguson** +* fix fujifilm_ratings running on all images +* added string library functions to sanitize windows io.popen and os.execute functions +* added database maintenance script + +**05 Jun 2024 - wpferguson** +* added fix for executable_manager not being visible on windows + +**30 May 2024 - kkotowicz** +* open in explorer now uses applescript on macos to open multiple files + +**20 May 2024 - wpferguson** +* added string variable substitution to the string library + +**16 May 2024 - wpferguson** +* fix crash in script_manager + +**15 May 2024 - wpferguson** +* added metadata to scripts (name, author, purpose, help url) + +**06 May 2024 - christian.sueltrop** +* added passport_guide_germany script + +**08 Apr 2024 - wpferguson** +* made script_manager aware of library modules in other directories besides lib + +**29 Mar 2024 - wpferguson** +* updated examples/gui_action to use NaN instead of nan + +**29 Mar 2024 - dterrahe** +* add lua action script example + +**28 Jan 2024 - wpferguson** +* fix script_manager crash when script existed in top level lua directory + +**24 Jan 2024 - wpferguson** +* don't set lib visibility unless we are in lighttable mode + +**15 Jan 2024 - MStraeten** +* update x-touch.lua with support for primaries slider + +**14 Jan 2024 - wpferguson** +* added cycle_group_leader script +* added hif_group_leader script +* added jpg_group_leader script + +**28 Oct 2023 - ddittmar** +* Added select non existing image script + +**17 Oct 2023 - wpferguson** +* script_manager wrap username in quotes to handle spaces in username + +**20 Sep 2023 - wpferguson** +* script_manager explicitly set stopped scripts to false + +**18 Aug 2023 - wpferguson** +* swap the position of executable_manager and script_manager due to windows gtk bug + +**17 Jul 2023 - wpferguson** +* update check for update instructions +* update flatpak readme + +**16 Jul 2023 - wpferguson** +* added script_data.show function to contrib/gpx_export + +**15 Jul 2023 - wpferguson** +* script_manager updates +* added check_max_api_version for the case where the API no longer supports the required + functions + +**14 Jul 2023 - spaceChRis** +* add tooltip to filter manager + **25 Mar 2023 - wpferguson** * Added script_manager darktable shortcut integration * Moved filename/path/extension string functions to string library diff --git a/README.md b/README.md index f371e8d5..8caa5c2e 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,17 @@ These scripts are written primarily by the darktable developers and maintained b Name|Standalone|OS |Purpose ----|:--------:|:---:|------- -[check_for_updates](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/official/check_for_updates)|Yes|LMW|Check for updates to darktable -[copy_paste_metadata](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/official/copy_paste_metadata)|Yes|LMW|Copy and paste metadata, tags, ratings, and color labels between images -[delete_long_tags](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/official/delete_long_tags)|Yes|LMW|Delete all tags longer than a specified length -[delete_unused_tags](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/official/delete_unused_tags)|Yes|LMW|Delete tags that have no associated images -[enfuse](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/official/enfuse)|No|L|Exposure blend several images (HDR) -[generate_image_txt](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/official/generate_image_txt)|No|L|Generate txt sidecar files to be overlaid on zoomed images -[image_path_in_ui](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/official/image_path_in_ui)|Yes|LMW|Plugin to display selected image path -[import_filter_manager](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/official/import_filter_manager)|Yes|LMW|Manager for import filters -[import_filters](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/official/import_filters)|No|LMW|Two import filters for use with import_filter_manager -[save_selection](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/official/save_selection)|Yes|LMW|Provide save and restore from multiple selection buffers -[selection_to_pdf](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/official/selection_to_pdf)|No|L|Generate a PDF file from the selected images +[check_for_updates](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/check_for_updates)|Yes|LMW|Check for updates to darktable +[copy_paste_metadata](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/copy_paste_metadata)|Yes|LMW|Copy and paste metadata, tags, ratings, and color labels between images +[delete_long_tags](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/delete_long_tags)|Yes|LMW|Delete all tags longer than a specified length +[delete_unused_tags](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/delete_unused_tags)|Yes|LMW|Delete tags that have no associated images +[enfuse](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/enfuse)|No|L|Exposure blend several images (HDR) +[generate_image_txt](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/generate_image_txt)|No|L|Generate txt sidecar files to be overlaid on zoomed images +[image_path_in_ui](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/image_path_in_ui)|Yes|LMW|Plugin to display selected image path +[import_filter_manager](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/import_filter_manager)|Yes|LMW|Manager for import filters +[import_filters](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/import_filters)|No|LMW|Two import filters for use with import_filter_manager +[save_selection](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/save_selection)|Yes|LMW|Provide save and restore from multiple selection buffers +[selection_to_pdf](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/selection_to_pdf)|No|L|Generate a PDF file from the selected images ### Contributed Scripts @@ -34,43 +34,50 @@ These scripts are contributed by users. They are meant to have an "owner", i.e. Name|Standalone|OS |Purpose ----|:--------:|:---:|------- -[AutoGrouper](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/autogrouper)|Yes|LMW|Group images together by time -[autostyle](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/autostyle)|Yes|LMW|Automatically apply styles on import +[AutoGrouper](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/autogrouper)|Yes|LMW|Group images together by time +[autostyle](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/autostyle)|Yes|LMW|Automatically apply styles on import change_group_leader|Yes|LMW|Change which image leads group -[clear_GPS](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/clear_gps)|Yes|LMW|Reset GPS information for selected images -[CollectHelper](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/collecthelper)|Yes|LMW|Add buttons to selected images module to manipulate the collection -[copy_attach_detach_tags](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/copy_attach_detach_tags)|Yes|LMW|Copy and paste tags from/to images -[cr2hdr](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/cr2hdr)|Yes|L|Process image created with Magic Lantern Dual ISO -[enfuseAdvanced](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/enfuseadvanced)|No|LMW|Merge multiple images into Dynamic Range Increase (DRI) or Depth From Focus (DFF) images -[exportLUT](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/exportlut)|Yes|LMW|Create a LUT from a style and export it -[ext_editor](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/ext_editor)|No|LW|Export pictures to collection and edit them with up to nine user-defined external editors -[face_recognition](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/face_recognition)|No|LM|Identify and tag images using facial recognition -[fujifilm_dynamic_range](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/fujifilm_dynamic_range)|No|LMW|Correct fujifilm exposure based on exposure bias camera setting -[fujifilm_ratings](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/fujifilm_ratings)|No|LM|Support importing Fujifilm ratings -[geoJSON_export](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/geojson_export)|No|L|Create a geo JSON script with thumbnails for use in ... -[geoToolbox](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/geotoolbox)|No|LMW|A toolbox of geo functions -[gimp](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/gimp)|No|LMW|Open an image in GIMP for editing and return the result -[gpx_export](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/gpx_export)|No|LMW|Export a GPX track file from selected images GPS data -[HDRMerge](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/hdrmerge)|No|LMW|Combine the selected images into an HDR DNG and return the result -[hugin](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/hugin)|No|LMW|Combine selected images into a panorama and return the result -[image_stack](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/image_stack)|No|LMW|Combine a stack of images to remove noise or transient objects -[image_time](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/image_time)|Yes|LMW|Adjust the EXIF image time -[kml_export](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/kml_export)|No|L|Export photos with a KML file for usage in Google Earth -[LabelsToTags](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/labelstotags)|Yes|LMW|Apply tags based on color labels and ratings -[OpenInExplorer](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/openinexplorer)|No|LMW|Open the selected images in the system file manager -[passport_guide](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/passport_guide)|Yes|LMW|Add passport cropping guide to darkroom crop tool -[pdf_slideshow](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/pdf_slideshow)|No|LM|Export images to a PDF slideshow -[photils](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/photils)|No|LM|Automatic tag suggestions for your images -[quicktag](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/quicktag)|Yes|LMW|Create shortcuts for quickly applying tags -[rate_group](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/rate_group)|Yes|LMW|Apply or remove a star rating from grouped images -[rename_images](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/rename_images)|Yes|LMW|Rename single or multiple images -[rename-tags](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/rename-tags)|Yes|LMW|Change a tag name -[RL_out_sharp](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/rl_out_sharp)|No|LW|Output sharpening using GMic (Richardson-Lucy algorithm) -[select_non_existing](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/select_non_existing)|Yes|LMW|Enable selection of non-existing images in the the currently worked on images -[select_untagged](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/select_untagged)|Yes|LMW|Enable selection of untagged images -[slideshowMusic](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/slideshowMusic)|No|L|Play music during a slideshow -[transfer_hierarchy](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/transfer_hierarchy)|Yes|LMW|Image move/copy preserving directory hierarchy -[video_ffmpeg](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/video_ffmpeg)|No|LMW|Export video from darktable +[clear_GPS](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/clear_gps)|Yes|LMW|Reset GPS information for selected images +[CollectHelper](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/collecthelper)|Yes|LMW|Add buttons to selected images module to manipulate the collection +color_profile_manager|Yes|LMW|Manage darktable input and output color profiles +[copy_attach_detach_tags](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/copy_attach_detach_tags)|Yes|LMW|Copy and paste tags from/to images +[cr2hdr](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/cr2hdr)|Yes|L|Process image created with Magic Lantern Dual ISO +cycle_group_leader|Yes|LMW|cycle through images of a group making each the group leader in turn +dbmaint|Yes|LMW|find and remove database entries for missing film rolls and images +[enfuseAdvanced](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/enfuseadvanced)|No|LMW|Merge multiple images into Dynamic Range Increase (DRI) or Depth From Focus (DFF) images +[exportLUT](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/exportlut)|Yes|LMW|Create a LUT from a style and export it +[ext_editor](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/ext_editor)|No|LW|Export pictures to collection and edit them with up to nine user-defined external editors +[face_recognition](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/face_recognition)|No|LM|Identify and tag images using facial recognition +[fujifilm_dynamic_range](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/fujifilm_dynamic_range)|No|LMW|Correct fujifilm exposure based on exposure bias camera setting +[fujifilm_ratings](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/fujifilm_ratings)|No|LM|Support importing Fujifilm ratings +[geoJSON_export](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/geojson_export)|No|L|Create a geo JSON script with thumbnails for use in ... +[geoToolbox](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/geotoolbox)|No|LMW|A toolbox of geo functions +[gimp](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/gimp)|No|LMW|Open an image in GIMP for editing and return the result +[gpx_export](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/gpx_export)|No|LMW|Export a GPX track file from selected images GPS data +harmonic_armature|Yes|LMW|provide a harmonic armature guide +hif_group_leader|Yes|LMW|change the group leader in a raw+heif image pair to the heif image +[HDRMerge](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/hdrmerge)|No|LMW|Combine the selected images into an HDR DNG and return the result +[hugin](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/hugin)|No|LMW|Combine selected images into a panorama and return the result +[image_stack](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/image_stack)|No|LMW|Combine a stack of images to remove noise or transient objects +[image_time](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/image_time)|Yes|LMW|Adjust the EXIF image time +jpg_group_leader|Yes|LMW|change the group leader for a raw+jpg pair to the jpg image +[kml_export](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/kml_export)|No|L|Export photos with a KML file for usage in Google Earth +[LabelsToTags](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/labelstotags)|Yes|LMW|Apply tags based on color labels and ratings +[OpenInExplorer](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/openinexplorer)|No|LMW|Open the selected images in the system file manager +[passport_guide](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/passport_guide)|Yes|LMW|Add passport cropping guide to darkroom crop tool +passport_guide_germany|Yes|LMW|Add passport cropping guide for German passports to darkroom crop tool +[pdf_slideshow](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/pdf_slideshow)|No|LM|Export images to a PDF slideshow +[photils](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/photils)|No|LM|Automatic tag suggestions for your images +[quicktag](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/quicktag)|Yes|LMW|Create shortcuts for quickly applying tags +[rate_group](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/rate_group)|Yes|LMW|Apply or remove a star rating from grouped images +[rename_images](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/rename_images)|Yes|LMW|Rename single or multiple images +[rename-tags](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/rename-tags)|Yes|LMW|Change a tag name +[RL_out_sharp](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/rl_out_sharp)|No|LW|Output sharpening using GMic (Richardson-Lucy algorithm) +[select_non_existing](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/select_non_existing)|Yes|LMW|Enable selection of non-existing images in the the currently worked on images +[select_untagged](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/select_untagged)|Yes|LMW|Enable selection of untagged images +[slideshowMusic](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/slideshowMusic)|No|L|Play music during a slideshow +[transfer_hierarchy](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/transfer_hierarchy)|Yes|LMW|Image move/copy preserving directory hierarchy +[video_ffmpeg](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/video_ffmpeg)|No|LMW|Export video from darktable ### Example Scripts @@ -78,17 +85,19 @@ These scripts provide examples of how to use specific portions of the API. They Name|Standalone|OS |Purpose ----|:--------:|:---:|------- -[api_version](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/examples/api_version)|Yes|LMW|Print the current API version -[darkroom_demo](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/examples/darkroom_demo)|Yes|LMW|Demonstrate changing images in darkoom -[gettextExample](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/examples/gettextexample)|Yes|LM|How to use translation -[hello_world](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/examples/hello_world)|Yes|LMW|Prints hello world when darktable starts -[lighttable_demo](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/examples/lighttable_demo)|Yes|LMW|Demonstrate controlling lighttable mode, zoom, sorting and filtering -[moduleExample](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/examples/moduleexample)|Yes|LMW|How to create a lighttable module -[multi_os](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/examples/multi_os)|No|LMW|How to create a cross platform script that calls an external executable -[panels_demo](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/examples/panels_demo)|Yes|LMW|Demonstrate hiding and showing darktable panels -[preferenceExamples](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/examples/preferenceexamples)|Yes|LMW|How to use preferences in a script -[printExamples](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/examples/printexamples)|Yes|LMW|How to use various print functions from a script -[running_os](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/examples/running_os)|Yes|LMW|Print out the running operating system +[api_version](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/api_version)|Yes|LMW|Print the current API version +[darkroom_demo](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/darkroom_demo)|Yes|LMW|Demonstrate changing images in darkoom +[gettextExample](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/gettextexample)|Yes|LM|How to use translation +gui_actions|Yes|LMW|demonstrate controlling the GUI using darktable.gui.action calls +[hello_world](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/hello_world)|Yes|LMW|Prints hello world when darktable starts +[lighttable_demo](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/lighttable_demo)|Yes|LMW|Demonstrate controlling lighttable mode, zoom, sorting and filtering +[moduleExample](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/moduleexample)|Yes|LMW|How to create a lighttable module +[multi_os](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/multi_os)|No|LMW|How to create a cross platform script that calls an external executable +[panels_demo](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/panels_demo)|Yes|LMW|Demonstrate hiding and showing darktable panels +[preferenceExamples](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/preferenceexamples)|Yes|LMW|How to use preferences in a script +[printExamples](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/printexamples)|Yes|LMW|How to use various print functions from a script +[running_os](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/running_os)|Yes|LMW|Print out the running operating system +x-touch|Yes|LMW|demonstrate how to use an x-touch mini MIDI controller to control the darktable GUI ### Tools @@ -96,11 +105,11 @@ Tool scripts perform functions relating to the repository, such as generating do Name|Standalone|OS |Purpose ----|:--------:|:---:|------- -[executable_manager](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/tools/executable_manager)|Yes|LMW|Manage the external executables used by the lua scripts -[gen_i18n_mo](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/tools/gen_i18n_mo)|No|LMW|Generate compiled translation files (.mo) from source files (.po) -[get_lib_manpages](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/tools/get_lib_manpages)|No|LM|Retrieve the library documentation and output it in man page and PDF format -[get_libdoc](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/tools/get_libdoc)|No|LMW|Retrieve the library documentation and output it as text -[script_manager](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/tools/script_manager)|No|LMW|Manage (install, update, enable, disable) the lua scripts +[executable_manager](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/tools/executable_manager)|Yes|LMW|Manage the external executables used by the lua scripts +[gen_i18n_mo](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/tools/gen_i18n_mo)|No|LMW|Generate compiled translation files (.mo) from source files (.po) +[get_lib_manpages](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/tools/get_lib_manpages)|No|LM|Retrieve the library documentation and output it in man page and PDF format +[get_libdoc](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/tools/get_libdoc)|No|LMW|Retrieve the library documentation and output it as text +[script_manager](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/tools/script_manager)|No|LMW|Manage (install, update, enable, disable) the lua scripts ### Related third-party projects @@ -118,6 +127,8 @@ The following third-party projects are listed for information only. Think of thi * [nbremond77/darktable](https://github.com/nbremond77/darktable/tree/master/scripts) * [s5k6/dtscripts](https://github.com/s5k6/dtscripts) * [ChristianBirzer/darktable_extra_scripts](https://github.com/ChristianBirzer/darktable_extra_scripts) +* [fjb2020/darktable-scripts](https://github.com//fjb2020/darktable-scripts) +* [bastibe/Darktable-Film-Simulation-Panel](https://github.com/bastibe/Darktable-Film-Simulation-Panel) ## Download and Install @@ -226,18 +237,18 @@ To update the script repository, open a terminal or command prompt and do the fo ## Documentation -The [Lua Scripts Manual](https://darktable-org.github.io/luadocs/lua.scripts.manual/) provides documentation +The [Lua Scripts Manual](https://docs.darktable.org/lua/stable/lua.scripts.manual/) provides documentation for the scripts transcribed from the header comments. Each script also contains comments and usage instructions in the header comments. -The [Lua Scripts Library API Manual](https://darktable-org.github.io/luadocs/lua.scripts.api.manual/) provides +The [Lua Scripts Library API Manual](https://docs.darktable.org/lua/stable/lua.scripts.api.manual/) provides documentation of the libraries and functions. Lua-script libraries documentation may also be generated using the tools in the tools/ directory. More information about the scripting with Lua can be found in the darktable user manual: -[Scripting with Lua](https://darktable-org.github.io/dtdocs/lua/) +[Scripting with Lua](https://darktable.org.github.io/dtdocs/lua/) -The [Lua API Manual](https://darktable-org.github.io/luadocs/lua.api.manual/) provides docuemntation of the +The [Lua API Manual](https://docs.darktable.org/lua/stable/lua.api.manual/) provides docuemntation of the darktable Lua API. ## Troubleshooting diff --git a/contrib/AutoGrouper.lua b/contrib/AutoGrouper.lua index 90f65dbc..889c9af1 100644 --- a/contrib/AutoGrouper.lua +++ b/contrib/AutoGrouper.lua @@ -43,11 +43,10 @@ du.check_min_api_version("7.0.0", "AutoGrouper") local MOD = 'autogrouper' -local gettext = dt.gettext -gettext.bindtextdomain(MOD, dt.configuration.config_dir .."/lua/locale/") +local gettext = dt.gettext.gettext local function _(msgid) - return gettext.gettext(msgid) + return gettext(msgid) end -- return data structure for script_manager @@ -55,7 +54,7 @@ end local script_data = {} script_data.metadata = { - name = "AutoGrouper", + name = _("auto group"), purpose = _("automatically group images by time interval"), author = "Kevin Ertel", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/AutoGrouper/" @@ -133,10 +132,11 @@ local function main(on_collection) for i, image in ipairs(images) do if i == 1 then prev_image = image + image:make_group_leader() elseif string.match(image.exif_datetime_taken, '[%d]') ~= nil then --make sure current image has a timestamp, if so check if it is within the user specified gap value and add to group local curr_image = image if GetTimeDiff(curr_image, prev_image) <= GUI.gap.value then - images[i]:group_with(images[i-1]) + images[i]:group_with(images[i-1].group_leader) end prev_image = curr_image end diff --git a/contrib/CollectHelper.lua b/contrib/CollectHelper.lua index ca76d857..978aa309 100644 --- a/contrib/CollectHelper.lua +++ b/contrib/CollectHelper.lua @@ -53,8 +53,6 @@ local all_active = false du.check_min_api_version("7.0.0", "CollectHelper") -dt.gettext.bindtextdomain("CollectHelper", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -64,7 +62,7 @@ end local script_data = {} script_data.metadata = { - name = "CollectHelper", + name = _("collection helper"), purpose = _("add collection helper buttons"), author = "Kevin Ertel", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/CollectHelper/" @@ -239,7 +237,7 @@ end dt.preferences.register("module_CollectHelper", "all_and", -- name "bool", -- type _('CollectHelper: all'), -- label - _('will create a collect parameter set that utilizes all enabled CollectHelper types (and)'), -- tooltip + _('creates a collect parameter set that utilizes all enabled CollectHelper types (and)'), -- tooltip true -- default ) dt.preferences.register("module_CollectHelper", "colors", -- name diff --git a/contrib/HDRMerge.lua b/contrib/HDRMerge.lua index 5ab1bee2..9d7a951d 100644 --- a/contrib/HDRMerge.lua +++ b/contrib/HDRMerge.lua @@ -54,8 +54,6 @@ du.check_min_api_version("7.0.0", "HDRmerge") -- Tell gettext where to find the .mo file translating messages for a particular domain local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("HDRMerge", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -65,7 +63,7 @@ end local script_data = {} script_data.metadata = { - name = "HDRmerge", + name = _("HDR merge"), purpose = _("merge bracketed images into an HDR DNG image"), author = "Kevin Ertel", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/HDRmerge" @@ -218,7 +216,7 @@ local function main() PreCall({HDRM}) --check if furst run then check if install OK if HDRM.install_error then dt.print_error('HDRMerge install issue') - dt.print(_('HDRMerge install issue, please ensure the binary path is proper')) + dt.print(_('HDRMerge install issue, please ensure the binary path is correct')) return end images = dt.gui.selection() --get selected images @@ -416,14 +414,14 @@ GUI.Target.copy_tags = dt.new_widget('check_button'){ temp = dt.preferences.read(mod, 'active_add_tags', 'string') if temp == '' then temp = nil end GUI.Target.add_tags = dt.new_widget('entry'){ - tooltip = _('additional tags to be added on import, seperate with commas, all spaces will be removed'), + tooltip = _('additional tags to be added on import, separate with commas, all spaces will be removed'), text = temp, - placeholder = _('enter tags, seperated by commas'), + placeholder = _('enter tags, separated by commas'), editable = true } GUI.run = dt.new_widget('button'){ label = _('merge'), - tooltip =_('run HDRMerge with the above specified settings'), + tooltip =_('run HDRMerge with the above settings'), clicked_callback = function() main() end } GUI.exes.HDRMerge = dt.new_widget('file_chooser_button'){ diff --git a/contrib/LabelsToTags.lua b/contrib/LabelsToTags.lua index 6046f9c3..32031a3b 100644 --- a/contrib/LabelsToTags.lua +++ b/contrib/LabelsToTags.lua @@ -54,8 +54,6 @@ du.check_min_api_version("7.0.0", "LabelsToTags") local gettext = darktable.gettext.gettext -darktable.gettext.bindtextdomain("LabelsToTags", darktable.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -65,7 +63,7 @@ end local script_data = {} script_data.metadata = { - name = "LabelsToTags", + name = _("labels to tags"), purpose = _("allows the mass-application of tags using color labels and ratings as a guide"), author = "August Schwerdfeger (august@schwerdfeger.name)", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/LabelsToTags" @@ -130,10 +128,10 @@ local initialAvailableMappings = { ["***+**"] = { _("blue") }, ["****+*"] = { _("purple") } }, [_("single colors")] = { ["+----*"] = { _("red"), _("only red") }, - ["-+---*"] = { _("Yellow"), _("only yellow") }, - ["--+--*"] = { _("Green"), _("only green") }, - ["---+-*"] = { _("Blue"), _("only blue") }, - ["----+*"] = { _("Purple"), _("only purple") } }, + ["-+---*"] = { _("yellow"), _("only yellow") }, + ["--+--*"] = { _("green"), _("only green") }, + ["---+-*"] = { _("blue"), _("only blue") }, + ["----+*"] = { _("purple"), _("only purple") } }, [_("ratings")] = { ["*****0"] = { _("no stars"), _("not rejected") }, ["*****1"] = { _("one star"), _("not rejected") }, ["*****2"] = { _("two stars"), _("not rejected") }, @@ -216,8 +214,8 @@ ltt.my_widget = darktable.new_widget("box") { orientation = "vertical", mappingComboBox, darktable.new_widget("button") { - label = "start", - tooltip = "Tag all selected images", + label = _("start"), + tooltip = _("tag all selected images"), clicked_callback = doTagging } } @@ -247,7 +245,7 @@ end local function install_module() if not ltt.module_installed then - darktable.register_lib(LIB_ID,"labels to tags",true,true,{ + darktable.register_lib(LIB_ID,_("labels to tags"),true,true,{ [darktable.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER",20}, },ltt.my_widget,nil,nil) ltt.module_installed = true diff --git a/contrib/OpenInExplorer.lua b/contrib/OpenInExplorer.lua index dba04d02..14f788d7 100644 --- a/contrib/OpenInExplorer.lua +++ b/contrib/OpenInExplorer.lua @@ -56,8 +56,6 @@ local gettext = dt.gettext.gettext --Check API version du.check_min_api_version("7.0.0", "OpenInExplorer") -dt.gettext.bindtextdomain("OpenInExplorer", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -67,7 +65,7 @@ end local script_data = {} script_data.metadata = { - name = "OpenInExplorer", + name = _("open in explorer"), purpose = _("open a selected file in the system file manager"), author = "Kevin Ertel", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/OpenInExplorer" @@ -118,9 +116,26 @@ open_dir.linux = [[busctl --user call org.freedesktop.FileManager1 /org/freedesk local open_files = {} open_files.windows = "explorer.exe /select, %s" -open_files.macos = "open -Rn %s" +open_files.macos = "osascript -e 'tell application \"Finder\" to (reveal {%s}) activate'" open_files.linux = [[busctl --user call org.freedesktop.FileManager1 /org/freedesktop/FileManager1 org.freedesktop.FileManager1 ShowItems ass %d %s ""]] +reveal_file_osx_cmd = "\"%s\" as POSIX file" + +--Call the osx Finder with each selected image selected. +--For images in multiple folders, Finder will open a separate window for each +local function call_list_of_files_osx(selected_images) + local cmds = {} + for _, image in pairs(selected_images) do + current_image = image.path..PS..image.filename + -- AppleScript needs double quoted strings, and the whole command is wrapped in single quotes. + table.insert(cmds, string.format(reveal_file_osx_cmd, string.gsub(string.gsub(current_image, "\"", "\\\""), "'", "'\"'\"'"))) + end + reveal_cmd = table.concat(cmds, ",") + run_cmd = string.format(open_files.macos, reveal_cmd) + dt.print_log("OSX run_cmd = "..run_cmd) + dsys.external_command(run_cmd) +end + --Call the file mangager for each selected image on Linux. --There is one call to busctl containing a list of all the image file names. local function call_list_of_files(selected_images) @@ -188,6 +203,8 @@ local function open_in_fmanager() else if act_os == "linux" then call_list_of_files(images) + elseif act_os == "macos" then + call_list_of_files_osx(images) else call_file_by_file(images) end diff --git a/contrib/RL_out_sharp.lua b/contrib/RL_out_sharp.lua index aa64b3f8..b65581c9 100644 --- a/contrib/RL_out_sharp.lua +++ b/contrib/RL_out_sharp.lua @@ -68,8 +68,6 @@ du.check_min_api_version("7.0.0", MODULE_NAME) -- translation local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("RL_out_sharp", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -79,7 +77,7 @@ local function _(msgid) local script_data = {} script_data.metadata = { - name = "RL_out_sharp", + name = _("RL output sharpening"), purpose = _("Richardson-Lucy output sharpening using GMic"), author = "Marco Carrarini ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/RL_out_sharp" @@ -99,7 +97,18 @@ if not dt.preferences.read(MODULE_NAME, "initialized", "bool") then dt.preferences.write(MODULE_NAME, "iterations", "string", "10") dt.preferences.write(MODULE_NAME, "jpg_quality", "string", "95") dt.preferences.write(MODULE_NAME, "initialized", "bool", true) - end +end + +-- preserve original image metadata in the output image ----------------------- +local function preserve_metadata(original, sharpened) + local exiftool = df.check_if_bin_exists("exiftool") + + if exiftool then + dtsys.external_command("exiftool -overwrite_original_in_place -tagsFromFile " .. original .. " " .. sharpened) + else + dt.print_log(MODULE .. " exiftool not found, metadata not preserved") + end +end -- setup export --------------------------------------------------------------- @@ -163,7 +172,10 @@ local function export2RL(storage, image_table, extra_data) if result ~= 0 then dt.print(_("sharpening error")) return - end + end + + -- copy metadata from input_file to output_file + preserve_metadata(input_file, output_file) -- delete temp image os.remove(temp_name) diff --git a/contrib/auto_snapshot.lua b/contrib/auto_snapshot.lua new file mode 100644 index 00000000..48f99f5b --- /dev/null +++ b/contrib/auto_snapshot.lua @@ -0,0 +1,132 @@ +--[[ + + auto_snapshot.lua - automatically take a snapshot when an image is loaded in darkroom + + Copyright (C) 2024 Bill Ferguson . + + This program 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. + + This program 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 this program. If not, see . +]] +--[[ + auto_snapshot - + + automatically take a snapshot when an image is loaded in darkroom + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + None + + USAGE + * start the script from script_manager + * open an image in darkroom + + BUGS, COMMENTS, SUGGESTIONS + Bill Ferguson + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local log = require "lib/dtutils.log" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "auto_snapshot" +local DEFAULT_LOG_LEVEL = log.error + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("7.0.0", MODULE) -- choose the minimum version that contains the features you need + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- I 1 8 N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- S C R I P T M A N A G E R I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local script_data = {} + +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 +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 + +script_data.metadata = { + name = _("auto snapshot"), -- name of script + purpose = _("automatically take a snapshot when an image is loaded in darkroom"), -- purpose of script + author = "Bill Ferguson ", -- your name and optionally e-mail address + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/auto_snapshot/" -- URL to help/documentation +} + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- L O G L E V E L +-- - - - - - - - - - - - - - - - - - - - - - - - + +log.log_level(DEFAULT_LOG_LEVEL) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local auto_snapshot = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- P R E F E R E N C E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.preferences.register(MODULE, "always_create_snapshot", "bool", "auto_snapshot - " .. _("always automatically create_snapshot"), + _("create a snapshot even if the image is altered"), false) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + dt.destroy_event(MODULE, "darkroom-image-loaded") +end + +script_data.destroy = destroy + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.register_event(MODULE, "darkroom-image-loaded", + function(event, clean, image) + local always = dt.preferences.read(MODULE, "always_create_snapshot", "bool") + if clean and always then + dt.gui.libs.snapshots.take_snapshot() + elseif clean and not image.is_altered then + dt.gui.libs.snapshots.take_snapshot() + end + + end +) + + +return script_data \ No newline at end of file diff --git a/contrib/autostyle.lua b/contrib/autostyle.lua index a12cd0ac..32add29b 100644 --- a/contrib/autostyle.lua +++ b/contrib/autostyle.lua @@ -39,11 +39,11 @@ GPLv2 local darktable = require "darktable" local du = require "lib/dtutils" local filelib = require "lib/dtutils.file" +local syslib = require "lib/dtutils.system" du.check_min_api_version("7.0.0", "autostyle") local gettext = darktable.gettext.gettext -darktable.gettext.bindtextdomain("autostyle", darktable.configuration.config_dir .."/lua/locale/") local function _(msgid) return gettext(msgid) @@ -53,8 +53,10 @@ end local script_data = {} +local have_not_printed_config_message = true + script_data.metadata = { - name = "autostyle", + name = _("auto style"), purpose = _("automatically apply a style based on image EXIF tag"), author = "Marc Cousin ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/autostyle/" @@ -101,56 +103,62 @@ end local function autostyle_apply_one_image (image) local pref = darktable.preferences.read("autostyle", "exif_tag", "string") - -- We need the tag, the value and the style_name provided from the configuration string - local tag, value, style_name = string.match(pref, "(%g+)%s*=%s*(%g+)%s*=>%s*(%g+)") - -- check they all exist (correct syntax) - if (not tag) then - darktable.print(string.format(_("EXIF tag not found in %s"), pref)) - return 0 - end - if (not value) then - darktable.print(string.format(_("value to match not found in %s"), pref)) - return 0 - end - if (not style_name) then - darktable.print(string.format(_("style name not found in %s"), pref)) - return 0 - end - if not filelib.check_if_bin_exists("exiftool") then - darktable.print(_("can't find exiftool")) - return 0 - end - - - -- First find the style (we have its name) - local styles = darktable.styles - local style - for _, s in ipairs(styles) do - if s.name == style_name then - style = s - end - end - if (not style) then - darktable.print(string.format(_("style not found for autostyle: %s"), style_name)) - return 0 - end - - -- Apply the style to image, if it is tagged - local ok, auto_dr_attr = pcall(exiftool_attribute, image.path .. '/' .. image.filename,tag) - --darktable.print_error("dr_attr:" .. auto_dr_attr) - -- If the lookup fails, stop here - if (not ok) then - darktable.print(string.format(_("couldn't get attribute %s from exiftool's output"), auto_dr_attr)) - return 0 - end - if auto_dr_attr == value then - darktable.print_log("Image " .. image.filename .. ": autostyle automatically applied " .. pref) - darktable.styles.apply(style,image) - return 1 - else - darktable.print_log("Image " .. image.filename .. ": autostyle not applied, exif tag " .. pref .. " not matched: " .. auto_dr_attr) - return 0 + if pref and string.len(pref) >= 6 then + -- We need the tag, the value and the style_name provided from the configuration string + local tag, value, style_name = string.match(pref, "(%g+)%s*=%s*([%g ]-)%s*=>%s*(%g+)") + + -- check they all exist (correct syntax) + if (not tag) then + darktable.print(string.format(_("EXIF tag not found in %s"), pref)) + return 0 + end + if (not value) then + darktable.print(string.format(_("value to match not found in %s"), pref)) + return 0 + end + if (not style_name) then + darktable.print(string.format(_("style name not found in %s"), pref)) + return 0 + end + if not filelib.check_if_bin_exists("exiftool") then + darktable.print(_("can't find exiftool")) + return 0 + end + + + -- First find the style (we have its name) + local styles = darktable.styles + local style + for _, s in ipairs(styles) do + if s.name == style_name then + style = s + end + end + if (not style) then + darktable.print(string.format(_("style not found for autostyle: %s"), style_name)) + return 0 + end + + -- Apply the style to image, if it is tagged + local ok, auto_dr_attr = pcall(exiftool_attribute, image.path .. '/' .. image.filename,tag) + --darktable.print_error("dr_attr:" .. auto_dr_attr) + -- If the lookup fails, stop here + if (not ok) then + darktable.print(string.format(_("couldn't get attribute %s from exiftool's output"), auto_dr_attr)) + return 0 + end + if auto_dr_attr == value then + darktable.print_log("Image " .. image.filename .. ": autostyle automatically applied " .. pref) + darktable.styles.apply(style,image) + return 1 + else + darktable.print_log("Image " .. image.filename .. ": autostyle not applied, exif tag " .. pref .. " not matched: " .. auto_dr_attr) + return 0 + end + elseif have_not_printed_config_message then + have_not_printed_config_message = false + darktable.print(string.format(_("%s is not configured, please configure the preference in Lua options"), script_data.metadata.name)) end end @@ -180,11 +188,13 @@ end darktable.register_event("autostyle", "shortcut", autostyle_apply, _("apply your chosen style from exiftool tags")) -darktable.preferences.register("autostyle", "exif_tag", "string", "Autostyle: EXIF_tag=value=>style", _("apply a style automatically if an EXIF tag matches value, find the tag with exiftool"), "") +darktable.preferences.register("autostyle", "exif_tag", "string", + string.format("%s: EXIF_tag=value=>style", script_data.metadata.name), + _("apply a style automatically if an EXIF tag matches value, find the tag with exiftool"), "") darktable.register_event("autostyle", "post-import-image", autostyle_apply_one_image_event) script_data.destroy = destroy -return script_data \ No newline at end of file +return script_data diff --git a/contrib/change_group_leader.lua b/contrib/change_group_leader.lua index 202f3f6e..0e6f66e6 100644 --- a/contrib/change_group_leader.lua +++ b/contrib/change_group_leader.lua @@ -40,8 +40,6 @@ local gettext = dt.gettext.gettext local MODULE = "change_group_leader" -dt.gettext.bindtextdomain(MODULE, dt.configuration.config_dir .."/lua/locale/") - du.check_min_api_version("3.0.0", MODULE) local function _(msgid) @@ -53,7 +51,7 @@ end local script_data = {} script_data.metadata = { - name = "change_group_leader", + name = _("change group leader"), purpose = _("automatically change the leader of raw+jpg paired image groups"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/change_group_leader" @@ -65,7 +63,7 @@ script_data.restart = nil -- how to restart the (lib) script after it's been hid script_data.show = nil -- only required for libs since the destroy_method only hides them -- create a namespace to contain persistent data and widgets -chg_grp_ldr = {} +local chg_grp_ldr = {} local cgl = chg_grp_ldr @@ -82,7 +80,7 @@ local function install_module() if not cgl.module_installed then dt.register_lib( MODULE, -- Module name - _("change_group_leader"), -- Visible name + _("change group leader"), -- Visible name true, -- expandable false, -- resetable {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 700}}, -- containers @@ -127,7 +125,7 @@ end local function process_image_groups(images) if #images < 1 then - dt.print(_("o images selected")) + dt.print(_("no images selected")) dt.print_log(MODULE .. "no images seletected, returning...") else local mode = cgl.widgets.mode.value @@ -204,4 +202,4 @@ script_data.restart = restart script_data.destroy_method = "hide" script_data.show = restart -return script_data \ No newline at end of file +return script_data diff --git a/contrib/clear_GPS.lua b/contrib/clear_GPS.lua index f90d8b8f..2663ab4b 100644 --- a/contrib/clear_GPS.lua +++ b/contrib/clear_GPS.lua @@ -40,7 +40,6 @@ local dt = require "darktable" local du = require "lib/dtutils" local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("clear_GPS", dt.configuration.config_dir .."/lua/locale/") local function _(msgid) return gettext(msgid) @@ -51,7 +50,7 @@ end local script_data = {} script_data.metadata = { - name = "clear_GPS", + name = _("clear GPS info"), purpose = _("remove GPS data from selected image(s)"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/clear_gps/" @@ -92,7 +91,7 @@ dt.gui.libs.image.register_action( dt.register_event( "clear_GPS", "shortcut", function(event, shortcut) clear_GPS(dt.gui.action_images) end, - _("clear GPS data") + _("clear GPS data from selected images") ) return script_data diff --git a/contrib/color_profile_manager.lua b/contrib/color_profile_manager.lua index ed083178..683ae990 100644 --- a/contrib/color_profile_manager.lua +++ b/contrib/color_profile_manager.lua @@ -45,6 +45,7 @@ local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" +local dtsys = require "lib/dtutils.system" du.check_min_api_version("7.0.0", "color_profile_manager") @@ -54,8 +55,6 @@ du.check_min_api_version("7.0.0", "color_profile_manager") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("color_profile_manager", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -364,7 +363,7 @@ end local script_data = {} script_data.metadata = { - name = "color_profile_manager", + name = _("color profile manager"), purpose = _("manage external darktable color profiles"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/color_profile_manager" diff --git a/contrib/copy_attach_detach_tags.lua b/contrib/copy_attach_detach_tags.lua index 6d6c470c..3f1f6d34 100644 --- a/contrib/copy_attach_detach_tags.lua +++ b/contrib/copy_attach_detach_tags.lua @@ -44,8 +44,6 @@ local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "copy_attach_detach_tags") -dt.gettext.bindtextdomain("copy_attach_detach_tags", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -55,7 +53,7 @@ end local script_data = {} script_data.metadata = { - name = "copy_attach_detach_tags", + name = _("copy attach detach tags"), purpose = _("shortcuts to copy, paste, replace, or remove tags from images"), author = "Christian Kanzian", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/copy_attach_detach_tags" diff --git a/contrib/cr2hdr.lua b/contrib/cr2hdr.lua index a091a8f1..36f325f2 100644 --- a/contrib/cr2hdr.lua +++ b/contrib/cr2hdr.lua @@ -39,8 +39,6 @@ du.check_min_api_version("7.0.0", "cr2hdr") local gettext = darktable.gettext.gettext -darktable.gettext.bindtextdomain("cr2hdr", darktable.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -50,7 +48,7 @@ end local script_data = {} script_data.metadata = { - name = "cr2hdr", + name = _("cr2hdr"), purpose = _("process Magic Lantern dual ISO images"), author = "Till Theato ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/cr2hdr" diff --git a/contrib/cycle_group_leader.lua b/contrib/cycle_group_leader.lua index 30ed0b8e..e168e93b 100644 --- a/contrib/cycle_group_leader.lua +++ b/contrib/cycle_group_leader.lua @@ -60,8 +60,6 @@ du.check_min_api_version("7.0.0", MODULE) local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("cycle_group_leader", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -73,7 +71,7 @@ end local script_data = {} script_data.metadata = { - name = "cycle_group_leader", + name = _("cycle group leader"), purpose = _("change image group leader"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/cycle_group_leader" @@ -106,7 +104,7 @@ end local function cycle_group_leader(image) local group_images = image:get_group_members() if #group_images < 2 then - hinter_msg(_("no images to cycle to in group")) + hinter_msg(_("no images to cycle through in group")) return else local position = nil diff --git a/contrib/dbmaint.lua b/contrib/dbmaint.lua new file mode 100644 index 00000000..c08309a2 --- /dev/null +++ b/contrib/dbmaint.lua @@ -0,0 +1,334 @@ +--[[ + + dbmaint.lua - perform database maintenance + + Copyright (C) 2024 Bill Ferguson . + + This program 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. + + This program 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 this program. If not, see . +]] +--[[ + dbmaint - perform database maintenance + + Perform database maintenance to clean up missing images and filmstrips. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + None + + USAGE + * start dbmaint from script_manager + * scan for missing film rolls or missing images + * look at the results and choose to delete or not + + BUGS, COMMENTS, SUGGESTIONS + Bill Ferguson + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local log = require "lib/dtutils.log" + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "dbmaint" +local DEFAULT_LOG_LEVEL = log.error +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("7.0.0", MODULE) -- choose the minimum version that contains the features you need + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- I 1 8 N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- S C R I P T M A N A G E R I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local script_data = {} + +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 +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 + +script_data.metadata = { + name = _("db maintenance"), + purpose = _("perform database maintenance"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/dbmaint/" +} + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- L O G L E V E L +-- - - - - - - - - - - - - - - - - - - - - - - - + +log.log_level(DEFAULT_LOG_LEVEL) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local dbmaint = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- G L O B A L V A R I A B L E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dbmaint.main_widget = nil + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A L I A S E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local namespace = dbmaint + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +------------------- +-- helper functions +------------------- + +local function set_log_level(level) + local old_log_level = log.log_level() + log.log_level(level) + return old_log_level +end + +local function restore_log_level(level) + log.log_level(level) +end + +local function scan_film_rolls() + local missing_films = {} + + for _, filmroll in ipairs(dt.films) do + if not df.check_if_file_exists(filmroll.path) then + table.insert(missing_films, filmroll) + end + end + + return missing_films +end + +local function scan_images(film) + local old_log_level = set_log_level(DEFAULT_LOG_LEVEL) + local missing_images = {} + + if film then + for i = 1, #film do + local image = film[i] + log.msg(log.debug, "checking " .. image.filename) + if not df.check_if_file_exists(image.path .. PS .. image.filename) then + log.msg(log.info, image.filename .. " not found") + table.insert(missing_images, image) + end + end + end + + restore_log_level(old_log_level) + return missing_images +end + +local function remove_missing_film_rolls(list) + for _, filmroll in ipairs(list) do + filmroll:delete(true) + end +end + +-- force the lighttable to reload + +local function refresh_lighttable(film) + local rules = dt.gui.libs.collect.filter() + dt.gui.libs.collect.filter(rules) +end + +local function remove_missing_images(list) + local film = list[1].film + for _, image in ipairs(list) do + image:delete() + end + refresh_lighttable(film) +end + +local function install_module() + if not namespace.module_installed then + dt.register_lib( + MODULE, -- Module name + _("DB maintenance"), -- Visible name + true, -- expandable + true, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 0}}, -- containers + namespace.main_widget, + nil,-- view_enter + nil -- view_leave + ) + namespace.module_installed = true + end +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- U S E R I N T E R F A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +dbmaint.list_widget = dt.new_widget("text_view"){ + editable = false, + reset_callback = function(this) + this.text = "" + end +} + +dbmaint.chooser = dt.new_widget("combobox"){ + label = _("scan for"), + selected = 1, + _("film rolls"), _("images"), + reset_callback = function(this) + this.selected = 1 + end +} + +dbmaint.scan_button = dt.new_widget("button"){ + label = _("scan"), + tooltip = _("click to scan for missing film rolls/files"), + clicked_callback = function(this) + local found = nil + local found_text = "" + local old_log_level = set_log_level(DEFAULT_LOG_LEVEL) + log.msg(log.debug, "Button clicked") + if dbmaint.chooser.selected == 1 then -- film rolls + found = scan_film_rolls() + if #found > 0 then + for _, film in ipairs(found) do + local dir_name = du.split(film.path, PS) + found_text = found_text .. dir_name[#dir_name] .. "\n" + end + end + else + log.msg(log.debug, "checking path " .. dt.collection[1].path .. " for missing files") + found = scan_images(dt.collection[1].film) + if #found > 0 then + for _, image in ipairs(found) do + found_text = found_text .. image.filename .. "\n" + end + end + end + if #found > 0 then + log.msg(log.debug, "found " .. #found .. " missing items") + dbmaint.list_widget.text = found_text + dbmaint.found = found + dbmaint.remove_button.sensitive = true + else + log.msg(log.debug, "no missing items found") + end + restore_log_level(old_log_level) + end, + reset_callback = function(this) + dbmaint.found = nil + end +} + +dbmaint.remove_button = dt.new_widget("button"){ + label = _("remove"), + tooltip = _("remove missing film rolls/images"), + sensitive = false, + clicked_callback = function(this) + if dbmaint.chooser.selected == 1 then -- film rolls + remove_missing_film_rolls(dbmaint.found) + else + remove_missing_images(dbmaint.found) + end + dbmaint.found = nil + dbmaint.list_widget.text = "" + this.sensitive = false + end, + reset_callback = function(this) + this.sensitive = false + end +} + +dbmaint.main_widget = dt.new_widget("box"){ + orientation = "vertical", + dt.new_widget("section_label"){label = _("missing items")}, + dbmaint.list_widget, + dt.new_widget("label"){label = ""}, + dbmaint.chooser, + dt.new_widget("label"){label = ""}, + dbmaint.scan_button, + dbmaint.remove_button +} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + dt.gui.libs[MODULE].visible = false + + if namespace.event_registered then + dt.destroy_event(MODULE, "view-changed") + end + + return +end + +local function restart() + dt.gui.libs[MODULE].visible = true + + return +end + +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = restart + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not namespace.event_registered then + dt.register_event(MODULE, "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + namespace.event_registered = true + end +end + +return script_data \ No newline at end of file diff --git a/contrib/enfuseAdvanced.lua b/contrib/enfuseAdvanced.lua index b5ec586e..5027c564 100644 --- a/contrib/enfuseAdvanced.lua +++ b/contrib/enfuseAdvanced.lua @@ -70,8 +70,6 @@ du.check_min_api_version("7.0.0", "enfuseAdvanced") -- Tell gettext where to find the .mo file translating messages for a particular domain local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("enfuseAdvanced", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -81,7 +79,7 @@ end local script_data = {} script_data.metadata = { - name = "enfuseAdvanced", + name = _("enfuse advanced"), purpose = _("focus stack or exposure blend images"), author = "Kevin Ertel", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/enfuseAdvanced" @@ -445,7 +443,7 @@ local function main(storage, image_table, extra_data) dt.print(_('too few images selected, please select at least 2 images')) return elseif extra_data[1] == 2 then - dt.print(_('installation error, please verify binary paths are proper')) + dt.print(_('installation error, please verify binary paths are correct')) return end local images_to_remove = '' @@ -956,7 +954,7 @@ if temp == '' then temp = nil end GUI.Target.add_tags = dt.new_widget('entry'){ tooltip = _('additional tags to be added on import, seperate with commas, all spaces will be removed'), text = temp, - placeholder = _('enter tags, seperated by commas'), + placeholder = _('enter tags, separated by commas'), editable = true } temp = dt.preferences.read(mod, 'active_current_preset_ind', 'integer') @@ -1023,7 +1021,7 @@ GUI.Presets.variants_type = dt.new_widget('combobox'){ GUI.Presets.variants_type.sensitive = GUI.Presets.variants.value temp = df.get_executable_path_preference(AIS.name) GUI.exes.align_image_stack = dt.new_widget('file_chooser_button'){ - title = 'AIS binary path', + title = 'align_image_stack ' .. _('binary path'), value = temp, tooltip = temp, is_directory = false, @@ -1031,7 +1029,7 @@ GUI.exes.align_image_stack = dt.new_widget('file_chooser_button'){ } temp = df.get_executable_path_preference(ENF.name) GUI.exes.enfuse = dt.new_widget('file_chooser_button'){ - title = 'enfuse binary path', + title = 'enfuse ' .. _('binary path'), value = temp, tooltip = temp, is_directory = false, @@ -1039,7 +1037,7 @@ GUI.exes.enfuse = dt.new_widget('file_chooser_button'){ } temp = df.get_executable_path_preference(EXF.name) GUI.exes.exiftool = dt.new_widget('file_chooser_button'){ - title = 'Exiftool binary path', + title = 'exiftool ' .. _('binary path'), value = temp, tooltip = temp, is_directory = false, diff --git a/contrib/exportLUT.lua b/contrib/exportLUT.lua index 0a578a24..9ec29544 100644 --- a/contrib/exportLUT.lua +++ b/contrib/exportLUT.lua @@ -35,8 +35,6 @@ du.check_min_api_version("7.0.0", "exportLUT") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("exportLUT", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -46,7 +44,7 @@ end local script_data = {} script_data.metadata = { - name = "exportLUT", + name = _("export LUT"), purpose = _("export a style as a LUT"), author = "Noah Clarke", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/exportLUT" @@ -71,13 +69,13 @@ local mkdir_command = 'mkdir -p ' if dt.configuration.running_os == 'windows' then mkdir_command = 'mkdir ' end local file_chooser_button = dt.new_widget("file_chooser_button"){ - title = _("identity_file_chooser"), + title = _("choose the identity file"), value = "", is_directory = false } local export_chooser_button = dt.new_widget("file_chooser_button"){ - title = _("export_location_chooser"), + title = _("choose the export location"), value = "", is_directory = true } diff --git a/contrib/ext_editor.lua b/contrib/ext_editor.lua index 592550bc..be371e92 100644 --- a/contrib/ext_editor.lua +++ b/contrib/ext_editor.lua @@ -78,8 +78,6 @@ du.check_min_api_version("7.0.0", MODULE_NAME) -- translation local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("ext_editor", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -89,7 +87,7 @@ end local script_data = {} script_data.metadata = { - name = "ext_editor", + name = _("external editors"), purpose = _("edit images with external editors"), author = "Marco Carrarini, marco.carrarini@gmail.com", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/ext_editor" diff --git a/contrib/face_recognition.lua b/contrib/face_recognition.lua index 0f6183bd..0c60a5c1 100644 --- a/contrib/face_recognition.lua +++ b/contrib/face_recognition.lua @@ -54,8 +54,6 @@ local OUTPUT = dt.configuration.tmp_dir .. PS .. "facerecognition.txt" du.check_min_api_version("7.0.0", MODULE) -dt.gettext.bindtextdomain("face_recognition", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -65,7 +63,7 @@ end local script_data = {} script_data.metadata = { - name = "face_recognition", + name = _("face recognition"), purpose = _("use facial recognition to tag images"), author = "Sebastian Witt", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/face_recognition" diff --git a/contrib/fujifilm_dynamic_range.lua b/contrib/fujifilm_dynamic_range.lua index d373d130..37f79511 100644 --- a/contrib/fujifilm_dynamic_range.lua +++ b/contrib/fujifilm_dynamic_range.lua @@ -60,12 +60,11 @@ cameras may behave in other ways. local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" +local dtsys = require "lib/dtutils.system" local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "fujifilm_dynamic_range") -dt.gettext.bindtextdomain("fujifilm_dynamic_range", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -75,7 +74,7 @@ end local script_data = {} script_data.metadata = { - name = "fujifilm_dynamic_range", + name = _("fujifilm dynamic range"), purpose = _("compensate for Fujifilm raw files made using \"dynamic range\""), author = "Dan Torop ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/fujifilm_dynamic_range" @@ -125,6 +124,17 @@ local function detect_dynamic_range(event, image) -- note that scene-referred workflow exposure preset also pushes exposure up by 0.5 EV image.exif_exposure_bias = image.exif_exposure_bias + tonumber(raf_result) dt.print_log("[fujifilm_dynamic_range] raw exposure bias " .. tostring(raf_result)) + -- handle any duplicates + if #image:get_group_members() > 1 then + local basename = df.get_basename(image.filename) + local grouped_images = image:get_group_members() + for _, img in ipairs(grouped_images) do + if string.match(img.filename, basename) and img.duplicate_index > 0 then + -- its a duplicate + img.exif_exposure_bias = img.exif_exposure_bias + tonumber(raf_result) + end + end + end end local function destroy() diff --git a/contrib/fujifilm_ratings.lua b/contrib/fujifilm_ratings.lua index f9bb1c86..b29996d9 100644 --- a/contrib/fujifilm_ratings.lua +++ b/contrib/fujifilm_ratings.lua @@ -26,12 +26,11 @@ Dependencies: local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" +local dtsys = require "lib/dtutils.system" local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "fujifilm_ratings") -dt.gettext.bindtextdomain("fujifilm_ratings", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -41,7 +40,7 @@ end local script_data = {} script_data.metadata = { - name = "fujifilm_ratings", + name = _("fujifilm ratings"), purpose = _("import Fujifilm in-camera ratings"), author = "Ben Mendis ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/fujifilm_ratings" @@ -53,32 +52,35 @@ script_data.restart = nil -- how to restart the (lib) script after it's been hid script_data.show = nil -- only required for libs since the destroy_method only hides them local function detect_rating(event, image) + if not string.match(image.filename, "%.RAF$") and not string.match(image.filename, "%.raf$") then + return + end if not df.check_if_bin_exists("exiftool") then - dt.print_error(_("exiftool not found")) + dt.print_error("exiftool not found") return end local RAF_filename = df.sanitize_filename(tostring(image)) local JPEG_filename = string.gsub(RAF_filename, "%.RAF$", ".JPG") local command = "exiftool -Rating " .. JPEG_filename - dt.print_error(command) + dt.print_log(command) local output = io.popen(command) local jpeg_result = output:read("*all") output:close() if string.len(jpeg_result) > 0 then jpeg_result = string.gsub(jpeg_result, "^Rating.*(%d)", "%1") image.rating = tonumber(jpeg_result) - dt.print_error(string.format(_("using JPEG rating: %d"), jpeg_result)) + dt.print_log("using JPEG rating: " .. jpeg_result) return end command = "exiftool -Rating " .. RAF_filename - dt.print_error(command) + dt.print_log(command) output = io.popen(command) local raf_result = output:read("*all") output:close() if string.len(raf_result) > 0 then raf_result = string.gsub(raf_result, "^Rating.*(%d)", "%1") image.rating = tonumber(raf_result) - dt.print_error(string.format(_("using RAF rating: %d"), raf_result)) + dt.print_log("using RAF rating: " .. raf_result) end end diff --git a/contrib/geoJSON_export.lua b/contrib/geoJSON_export.lua index 695976a6..93e77289 100644 --- a/contrib/geoJSON_export.lua +++ b/contrib/geoJSON_export.lua @@ -35,12 +35,11 @@ USAGE local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" +local dtsys = require "lib/dtutils.system" local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "geoJSON_export") -dt.gettext.bindtextdomain("geoJSON_export", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -50,7 +49,7 @@ end local script_data = {} script_data.metadata = { - name = "geoJSON_export", + name = _("geoJSON export"), purpose = _("export a geoJSON file from geo data"), author = "Tobias Jakobs", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/geoJSON_export" @@ -91,19 +90,19 @@ end local function create_geoJSON_file(storage, image_table, extra_data) if not df.check_if_bin_exists("mkdir") then - dt.print_error(_("mkdir not found")) + dt.print_error("mkdir not found") return end if not df.check_if_bin_exists("convert") then - dt.print_error(_("convert not found")) + dt.print_error("convert not found") return end if not df.check_if_bin_exists("xdg-open") then - dt.print_error(_("xdg-open not found")) + dt.print_error("xdg-open not found") return end if not df.check_if_bin_exists("xdg-user-dir") then - dt.print_error(_("xdg-user-dir not found")) + dt.print_error("xdg-user-dir not found") return end @@ -294,7 +293,7 @@ local function create_geoJSON_file(storage, image_table, extra_data) file:write(geoJSON_file) file:close() - dt.print("geoJSON file created in "..exportDirectory) + dt.print(string.format(_("%s file created in %s"), "geoJSON", exportDirectory)) -- Open the file with the standard programm if ( dt.preferences.read("geoJSON_export","OpengeoJSONFile","bool") == true ) then @@ -314,19 +313,19 @@ end dt.preferences.register("geoJSON_export", "CreateMapBoxHTMLFile", "bool", - _("geoJSON export: Create an additional HTML file"), + "geoJSON export: ".._("Create an additional HTML file"), _("creates an HTML file that loads the geoJSON file. (needs a MapBox key"), false ) dt.preferences.register("geoJSON_export", "mapBoxKey", "string", - _("geoJSON export: MapBox key"), - _("https://www.mapbox.com/studio/account/tokens"), + "geoJSON export: MapBox " .. _("key"), + "https://www.mapbox.com/studio/account/tokens", '' ) dt.preferences.register("geoJSON_export", "OpengeoJSONFile", "bool", - _("geoJSON export: open geoJSON file after export"), + "geoJSON export: " .. _("open geoJSON file after export"), _("opens the geoJSON file after the export with the standard program for geoJSON files"), false ) diff --git a/contrib/geoToolbox.lua b/contrib/geoToolbox.lua index 83caa130..01a14eea 100644 --- a/contrib/geoToolbox.lua +++ b/contrib/geoToolbox.lua @@ -28,12 +28,11 @@ require "geoToolbox" local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" +local dtsys = require "lib/dtutils.system" local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "geoToolbox") -dt.gettext.bindtextdomain("geoToolbox", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -43,7 +42,7 @@ end local script_data = {} script_data.metadata = { - name = "geoToolbox", + name = _("geo toolbox"), purpose = _("geodata tools"), author = "Tobias Jakobs", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/geoToolbox" @@ -66,17 +65,17 @@ labelDistance.label = _("distance:") local label_copy_gps_lat = dt.new_widget("check_button") { - label = _("latitude:"), + label = _("latitude: "), value = true } local label_copy_gps_lon = dt.new_widget("check_button") { - label = _("longitude:"), + label = _("longitude: "), value = true } local label_copy_gps_ele = dt.new_widget("check_button") { - label = _("elevation:"), + label = _("elevation: "), value = true } -- @@ -168,7 +167,7 @@ local function get_first_coordinate() first_elevation = '' first_image_date = 0 - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if not image then first_have_data = false else @@ -207,7 +206,7 @@ local function get_second_coordinate() second_elevation = '' second_image_date = 0 - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if not image then second_have_data = false else @@ -243,7 +242,7 @@ local calc_in_between_slider = dt.new_widget("slider") --ToDo: this needs more love local function calc_in_between() local sel_images = dt.gui.action_images - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if image then image_date = make_time_stamp(image.exif_datetime_taken) if (first_have_data and second_have_data) then @@ -290,7 +289,7 @@ local function copy_gps() copy_gps_longitude = '' copy_gps_elevation = '' - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if not image then copy_gps_have_data = false else @@ -306,9 +305,9 @@ local function copy_gps() end end - label_copy_gps_lat.label = string.format(_("latitude: "), copy_gps_latitude) - label_copy_gps_lon.label = string.format(_("longitude: "),copy_gps_longitude) - label_copy_gps_ele.label = string.format(_("elevation: "), copy_gps_elevation) + label_copy_gps_lat.label = _("latitude: ") .. copy_gps_latitude + label_copy_gps_lon.label = _("longitude: ") .. copy_gps_longitude + label_copy_gps_ele.label = _("elevation: ") .. copy_gps_elevation return end @@ -317,7 +316,7 @@ end local function paste_gps(image) local sel_images = dt.gui.action_images - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if (label_copy_gps_lat.value) then image.latitude = copy_gps_latitude end @@ -344,7 +343,7 @@ local function open_location_in_gnome_maps() local i = 0; -- Use the first image with geo information - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if ((image.longitude and image.latitude) and (image.longitude ~= 0 and image.latitude ~= 90) -- Sometimes the north-pole but most likely just wrong data ) then @@ -371,12 +370,12 @@ end local function reverse_geocode() if not df.check_if_bin_exists("curl") then - dt.print_error(_("curl not found")) + dt.print_error("curl not found") return end if not df.check_if_bin_exists("jq") then - dt.print_error(_("jq not found")) + dt.print_error("jq not found") return end @@ -387,7 +386,7 @@ local function reverse_geocode() local i = 0; -- Use the first image with geo information - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if ((image.longitude and image.latitude) and (image.longitude ~= 0 and image.latitude ~= 90) -- Sometimes the north-pole but most likely just wrong data ) then @@ -462,7 +461,7 @@ local function calc_distance() local sel_images = dt.gui.selection() - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if ((image.longitude and image.latitude) and (image.longitude ~= 0 and image.latitude ~= 90) -- Sometimes the north-pole but most likely just wrong data ) then @@ -494,9 +493,9 @@ local function calc_distance() if (distance < 1) then distance = distance * 1000 - distanceUnit = _("m") + distanceUnit = "m" else - distanceUnit = _("km") + distanceUnit = "km" end return string.format(_("distance: %.2f %s"), distance, distanceUnit) @@ -546,7 +545,7 @@ local function altitude_profile() local elevationAdd = 0; local sel_images = dt.gui.action_images - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if ((not isnan(image.longitude) and not isnan(image.latitude) and not isnan(image.elevation) and image.elevation) and (image.longitude ~= 0 and image.latitude ~= 90) -- Sometimes the north-pole but most likely just wrong data ) then @@ -596,7 +595,7 @@ local function install_module() if not gT.module_installed then dt.register_lib( "geoToolbox", -- Module name - "geo toolbox", -- name + _("geo toolbox"), -- name true, -- expandable false, -- resetable {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers @@ -745,7 +744,7 @@ dt.preferences.register("geoToolbox", "mapBoxKey", "string", _("geoToolbox export: MapBox Key"), - _("https://www.mapbox.com/studio/account/tokens"), + "https://www.mapbox.com/studio/account/tokens", '' ) -- Register diff --git a/contrib/gimp.lua b/contrib/gimp.lua index 9a60cada..92c6d0ce 100644 --- a/contrib/gimp.lua +++ b/contrib/gimp.lua @@ -74,8 +74,6 @@ local gimp_widget = nil du.check_min_api_version("7.0.0", "gimp") -dt.gettext.bindtextdomain("gimp", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -85,7 +83,7 @@ end local script_data = {} script_data.metadata = { - name = "gimp", + name = _("edit with GIMP"), purpose = _("export and edit with GIMP"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/gimp" diff --git a/contrib/gpx_export.lua b/contrib/gpx_export.lua index b3d93015..b310f3a9 100644 --- a/contrib/gpx_export.lua +++ b/contrib/gpx_export.lua @@ -25,14 +25,12 @@ For each source folder, a separate is generated in the gpx file. local dt = require "darktable" local df = require "lib/dtutils.file" local dl = require "lib/dtutils" -local gettext = dt.gettext +local gettext = dt.gettext.gettext dl.check_min_api_version("7.0.0", "gpx_export") -gettext.bindtextdomain("gpx_export", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) - return gettext.dgettext("gpx_export", msgid) + return gettext(msgid) end -- return data structure for script_manager @@ -40,7 +38,7 @@ end local script_data = {} script_data.metadata = { - name = "gpx_export", + name = _("gpx export"), purpose = _("export gpx information to a file"), author = "Jannis_V", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/gpx_export" @@ -150,7 +148,7 @@ local function install_module() if not gpx.module_installed then dt.register_lib( "gpx_exporter", - "gpx export", + _("gpx export"), true, -- expandable true, -- resetable {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers diff --git a/contrib/harmonic_armature_guide.lua b/contrib/harmonic_armature_guide.lua index fd16f2b6..ffe03a29 100644 --- a/contrib/harmonic_armature_guide.lua +++ b/contrib/harmonic_armature_guide.lua @@ -36,8 +36,6 @@ local gettext = dt.gettext.gettext du.check_min_api_version("2.0.0", "harmonic_armature_guide") -dt.gettext.bindtextdomain("harmonic_armature_guide", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -45,7 +43,7 @@ end local script_data = {} script_data.metadata = { - name = "harmonic_armature_guide", + name = _("harmonic armature guide"), purpose = _("harmonic artmature guide"), author = "Hubert Kowalski", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/harmonic_armature_guide" diff --git a/contrib/hif_group_leader.lua b/contrib/hif_group_leader.lua index 26eaf48d..412eea07 100644 --- a/contrib/hif_group_leader.lua +++ b/contrib/hif_group_leader.lua @@ -64,8 +64,6 @@ du.check_min_api_version("7.0.0", MODULE) local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("hif_group_leader", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -77,7 +75,7 @@ end local script_data = {} script_data.metadata = { - name = "hif_group_leader", + name = _("HIF group leader"), purpose = _("make hif image group leader"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/hif_group_leader" diff --git a/contrib/hugin.lua b/contrib/hugin.lua index f209c0c5..46fbd90c 100644 --- a/contrib/hugin.lua +++ b/contrib/hugin.lua @@ -56,8 +56,6 @@ local PQ = dt.configuration.running_os == "windows" and '"' or "'" -- works with darktable API version from 5.0.0 on du.check_min_api_version("7.0.0", "hugin") -dt.gettext.bindtextdomain("hugin", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -67,7 +65,7 @@ end local script_data = {} script_data.metadata = { - name = "hugin", + name = _("hugin"), purpose = _("stitch images into a panorama"), author = "Wolfgang Goetz", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/hugin" @@ -85,7 +83,7 @@ end local function show_status(storage, image, format, filename, number, total, high_quality, extra_data) - dt.print("exporting to hugin: "..tostring(number).."/"..tostring(total)) + dt.print(string.format(_("exporting to hugin: %d / %d"), number, total)) end local function create_panorama(storage, image_table, extra_data) --finalize @@ -146,7 +144,7 @@ local function create_panorama(storage, image_table, extra_data) --finalize end if first_file == nil then - dt.print("no file selected") + dt.print(_("no file selected")) return end diff --git a/contrib/image_stack.lua b/contrib/image_stack.lua index c63ff166..661b28d3 100644 --- a/contrib/image_stack.lua +++ b/contrib/image_stack.lua @@ -73,8 +73,6 @@ local PS = dt.configuration.running_os == "windows" and "\\" or "/" -- works with LUA API version 5.0.0 du.check_min_api_version("7.0.0", "image_stack") -dt.gettext.bindtextdomain("image_stack", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -84,7 +82,7 @@ end local script_data = {} script_data.metadata = { - name = "image_stack", + name = _("image stack"), purpose = _("process a stack of images"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/image_stack" @@ -141,19 +139,19 @@ local chkbtn_will_align = dt.new_widget("check_button"){ local chkbtn_radial_distortion = dt.new_widget("check_button"){ label = _('optimize radial distortion for all images'), value = dt.preferences.read("align_image_stack", "def_radial_distortion", "bool"), - tooltip = _('optimize radial distortion for all images, \nexcept for first'), + tooltip = _('optimize radial distortion for all images, \nexcept the first'), } local chkbtn_optimize_field = dt.new_widget("check_button"){ label = _('optimize field of view for all images'), value = dt.preferences.read("align_image_stack", "def_optimize_field", "bool"), - tooltip =_('optimize field of view for all images, except for first. \nUseful for aligning focus stacks (DFF) with slightly \ndifferent magnification.'), + tooltip =_('optimize field of view for all images, except the first. \nUseful for aligning focus stacks (DFF) with slightly \ndifferent magnification.'), } local chkbtn_optimize_image_center = dt.new_widget("check_button"){ label = _('optimize image center shift for all images'), value = dt.preferences.read("align_image_stack", "def_optimize_image_center", "bool"), - tooltip =_('optimize image center shift for all images, \nexcept for first.'), + tooltip =_('optimize image center shift for all images, \nexcept the first.'), } local chkbtn_auto_crop = dt.new_widget("check_button"){ @@ -506,7 +504,7 @@ local function image_stack(storage, image_table, extra_data) return end - job = dt.gui.create_job("image stack", true, stop_job) + job = dt.gui.create_job(_("image stack"), true, stop_job) job.percent = job.percent + percent_step -- align images if requested diff --git a/contrib/image_time.lua b/contrib/image_time.lua index f20e5000..c0971306 100644 --- a/contrib/image_time.lua +++ b/contrib/image_time.lua @@ -107,6 +107,7 @@ local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" local ds = require "lib/dtutils.string" +local dtsys = require "lib/dtutils.system" local gettext = dt.gettext.gettext local img_time = {} @@ -115,8 +116,6 @@ img_time.event_registered = false du.check_min_api_version("7.0.0", "image_time") -dt.gettext.bindtextdomain("image_time", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -126,7 +125,7 @@ end local script_data = {} script_data.metadata = { - name = "image_time", + name = _("image time"), purpose = _("synchronize image time for images shot with different cameras or adjust or set image time"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/image_time" @@ -155,12 +154,12 @@ local function systime2exiftime(systime) end local function vars2exiftime(year, month, day, hour, min, sec) - local y = tonumber(year) and string.format("%4d", year) or " " - local mo = tonumber(month) and string.format("%02d", month) or " " - local d = tonumber(day) and string.format("%02d", day) or " " - local h = tonumber(hour) and string.format("%02d", hour) or " " - local m = tonumber(min) and string.format("%02d", min) or " " - local s = tonumber(sec) and string.format("%02d", sec) or " " + local y = tonumber(year) and string.format("%4d", year) or "0000" + local mo = tonumber(month) and string.format("%02d", month) or "00" + local d = tonumber(day) and string.format("%02d", day) or "00" + local h = tonumber(hour) and string.format("%02d", hour) or "00" + local m = tonumber(min) and string.format("%02d", min) or "00" + local s = tonumber(sec) and string.format("%02d", sec) or "00" return(y .. ":" .. mo .. ":" .. d .. " " .. h .. ":" .. m .. ":" .. s) end @@ -194,7 +193,7 @@ end local function synchronize_time(images) local sign = 1 - if img_time.sdir.value == "subtract" then + if img_time.sdir.value == _("subtract") then sign = -1 end synchronize_times(images, tonumber(img_time.diff_entry.text) * sign) @@ -256,7 +255,7 @@ local function _get_windows_image_file_creation_time(image) end p:close() else - dt.print(string.format(_("unable to get information for $s"), image.filename)) + dt.print(string.format(_("unable to get information for %s"), image.filename)) datetime = ERROR end return datetime @@ -510,7 +509,7 @@ img_time.stack = dt.new_widget("stack"){ dt.new_widget("box"){ orientation = "vertical", dt.new_widget("label"){label = _("set time")}, - dt.new_widget("section_label"){label = _("date: ")}, + dt.new_widget("section_label"){label = _("date:")}, img_time.sdy, img_time.smo, img_time.syr, diff --git a/contrib/jpg_group_leader.lua b/contrib/jpg_group_leader.lua index 6201b398..733071ed 100644 --- a/contrib/jpg_group_leader.lua +++ b/contrib/jpg_group_leader.lua @@ -64,8 +64,6 @@ du.check_min_api_version("7.0.0", MODULE) local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("jpg_group_leader", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -77,7 +75,7 @@ end local script_data = {} script_data.metadata = { - name = "jpg_group_leader", + name = _("JPG group leader"), purpose = _("make jpg image group leader"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/jpg_group_leader" diff --git a/contrib/kml_export.lua b/contrib/kml_export.lua index ec1aa04a..561724fa 100644 --- a/contrib/kml_export.lua +++ b/contrib/kml_export.lua @@ -45,8 +45,6 @@ local PS = dt.configuration.running_os == "windows" and "\\" or "/" du.check_min_api_version("7.0.0", "kml_export") -dt.gettext.bindtextdomain("kml_export", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -56,7 +54,7 @@ end local script_data = {} script_data.metadata = { - name = "kml_export", + name = _("kml export"), purpose = _("export KML/KMZ data to a file"), author = "Tobias Jakobs", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/kml_export" @@ -95,12 +93,12 @@ local function create_kml_file(storage, image_table, extra_data) end if not df.check_if_bin_exists(magickPath) then - dt.print_error(_("magick not found")) + dt.print_error("magick not found") return end if dt.configuration.running_os == "linux" then if not df.check_if_bin_exists("xdg-user-dir") then - dt.print_error(_("xdg-user-dir not found")) + dt.print_error("xdg-user-dir not found") return end end @@ -110,7 +108,7 @@ local function create_kml_file(storage, image_table, extra_data) if ( dt.preferences.read("kml_export","CreateKMZ","bool") == true and dt.configuration.running_os == "linux") then if not df.check_if_bin_exists("zip") then - dt.print_error(_("zip not found")) + dt.print_error("zip not found") return end exportDirectory = dt.configuration.tmp_dir diff --git a/contrib/passport_guide.lua b/contrib/passport_guide.lua index 246e7378..34e9fc79 100644 --- a/contrib/passport_guide.lua +++ b/contrib/passport_guide.lua @@ -41,8 +41,6 @@ local gettext = dt.gettext.gettext du.check_min_api_version("2.0.0", "passport_guide") -dt.gettext.bindtextdomain("passport_guide", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -50,7 +48,7 @@ end local script_data = {} script_data.metadata = { - name = "passport_guide", + name = _("passport guide"), purpose = _("guides for cropping passport photos"), author = "Kåre Hampf", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/passport_guide" diff --git a/contrib/passport_guide_germany.lua b/contrib/passport_guide_germany.lua index 735c092a..a1b9f618 100644 --- a/contrib/passport_guide_germany.lua +++ b/contrib/passport_guide_germany.lua @@ -40,21 +40,19 @@ USAGE local dt = require "darktable" local du = require "lib/dtutils" -local gettext = dt.gettext +local gettext = dt.gettext.gettext du.check_min_api_version("2.0.0", "passport_guide_germany") -- Tell gettext where to find the .mo file translating messages for a particular domain -dt.gettext.bindtextdomain("passport_guide_germany",dt.configuration.config_dir.."/lua/locale/") - local function _(msgid) - return gettext.dgettext("passport_guide_germany", msgid) + return gettext(msgid) end local script_data = {} script_data.metadata = { - name = "passport_guide_germany", + name = _("passport guide Germany"), purpose = _("guides for cropping passport and ID card (\"Personalausweis\") photos based on the \"Passbild-Schablone\" from the German Federal Ministry of the Interior and Community"), author = "menschmachine", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/passport_guide_germany" diff --git a/contrib/pdf_slideshow.lua b/contrib/pdf_slideshow.lua index 911493f8..b93ae978 100644 --- a/contrib/pdf_slideshow.lua +++ b/contrib/pdf_slideshow.lua @@ -44,8 +44,6 @@ local df = require "lib/dtutils.file" local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("pdf_slideshow", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -62,7 +60,7 @@ du.check_min_api_version("7.0.0", "pdf_slideshow") local script_data = {} script_data.metadata = { - name = "pdf_slideshow", + name = _("PDF slideshow"), purpose = _("generates a PDF slideshow (via Latex) containing all selected images one per slide"), author = "Pascal Obry", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/pdf_slideshow" diff --git a/contrib/photils.lua b/contrib/photils.lua index b06dc6ef..7bb39c6f 100644 --- a/contrib/photils.lua +++ b/contrib/photils.lua @@ -45,7 +45,6 @@ local MODULE_NAME = "photils" du.check_min_api_version("7.0.0", MODULE_NAME) local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("photils", dt.configuration.config_dir .."/lua/locale/") local function _(msgid) return gettext(msgid) @@ -56,7 +55,7 @@ end local script_data = {} script_data.metadata = { - name = "photils", + name = _("photils"), purpose = _("suggest tags based on image classification"), author = "Tobias Scheck", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/photils" @@ -339,7 +338,7 @@ function PHOTILS.on_tags_clicked() end if #PHOTILS.tags == 0 then - local msg = string.format(_("no tags where found"), MODULE_NAME) + local msg = string.format(_("no tags were found"), MODULE_NAME) GUI.warning_label.label = msg GUI.stack.active = GUI.error_view return @@ -394,7 +393,7 @@ end local function install_module() if not PHOTILS.module_installed then dt.register_lib(MODULE_NAME, - "photils autotagger", + _("photils auto-tagger"), true, true, PHOTILS.plugin_display_views, diff --git a/contrib/quicktag.lua b/contrib/quicktag.lua index 7a17fa1b..c218f09c 100644 --- a/contrib/quicktag.lua +++ b/contrib/quicktag.lua @@ -51,8 +51,6 @@ du.check_min_api_version("7.0.0", "quicktag") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("quicktag", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -62,7 +60,7 @@ end local script_data = {} script_data.metadata = { - name = "quicktag", + name = _("quick tag"), purpose = _("use buttons to quickly apply tags assigned to them"), author = "Christian Kanzian", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/quicktag" @@ -199,7 +197,7 @@ local function install_module() if not qt.module_installed then dt.register_lib( "quicktag", -- Module name - "quicktag", -- name + _("quick tag"), -- name true, -- expandable false, -- resetable {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 490}}, @@ -306,7 +304,7 @@ end for i=1,qnr do dt.register_event("quicktag " .. tostring(i), "shortcut", function(event, shortcut) tagattach(tostring(quicktag_table[i])) end, - string.format(_("quicktag %i"),i)) + string.format(_("quick tag %i"),i)) end script_data.destroy = destroy diff --git a/contrib/rate_group.lua b/contrib/rate_group.lua index 6455f6d3..0e9ddb29 100644 --- a/contrib/rate_group.lua +++ b/contrib/rate_group.lua @@ -46,8 +46,6 @@ du.check_min_api_version("7.0.0", "rate_group") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("rate_group", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -57,7 +55,7 @@ end local script_data = {} script_data.metadata = { - name = "rate_group", + name = _("rate group"), purpose = _("rate all images in a group"), author = "Dom H (dom@hxy.io)", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/rate_group" @@ -101,32 +99,33 @@ end, _("reject group")) dt.register_event("rg0", "shortcut", function(event, shortcut) apply_rating(0) -end, _("rate group 0")) + end, string.format(_("rate group %d"), 0) +) dt.register_event("rg1", "shortcut", function(event, shortcut) apply_rating(1) -end, _("rate group 1")) +end, string.format(_("rate group %d"), 1)) dt.register_event("rg2", "shortcut", function(event, shortcut) apply_rating(2) -end, _("rate group 2")) +end, string.format(_("rate group %d"), 2)) dt.register_event("rg3", "shortcut", function(event, shortcut) apply_rating(3) -end, _("rate group 3")) +end, string.format(_("rate group %d"), 3)) dt.register_event("rg4", "shortcut", function(event, shortcut) apply_rating(4) -end, _("rate group 4")) +end, string.format(_("rate group %d"), 4)) dt.register_event("rg5", "shortcut", function(event, shortcut) apply_rating(5) -end, _("rate group 5")) +end, string.format(_("rate group %d"), 5)) script_data.destroy = destroy diff --git a/contrib/rename-tags.lua b/contrib/rename-tags.lua index 3b84eba6..ae77056a 100644 --- a/contrib/rename-tags.lua +++ b/contrib/rename-tags.lua @@ -38,8 +38,6 @@ du.deprecated("contrib/rename-tags.lua","darktable release 4.0") local gettext = darktable.gettext.gettext -darktable.gettext.bindtextdomain("rename-tags", darktable.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -49,7 +47,7 @@ end local script_data = {} script_data.metadata = { - name = "rename-tags", + name = _("rename tags"), purpose = _("rename an existing tag"), author = "Sebastian Witt (se.witt@gmx.net)", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/rename-tags" diff --git a/contrib/rename_images.lua b/contrib/rename_images.lua index d3ed9824..0093c540 100644 --- a/contrib/rename_images.lua +++ b/contrib/rename_images.lua @@ -43,13 +43,12 @@ local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" +local ds = require "lib/dtutils.string" du.check_min_api_version("7.0.0", "rename_images") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("rename_images", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -57,12 +56,6 @@ end -- namespace variable local rename = { presets = {}, - substitutes = {}, - placeholders = {"ROLL_NAME","FILE_FOLDER","FILE_NAME","FILE_EXTENSION","ID","VERSION","SEQUENCE","YEAR","MONTH","DAY", - "HOUR","MINUTE","SECOND","EXIF_YEAR","EXIF_MONTH","EXIF_DAY","EXIF_HOUR","EXIF_MINUTE","EXIF_SECOND", - "STARS","LABELS","MAKER","MODEL","TITLE","CREATOR","PUBLISHER","RIGHTS","USERNAME","PICTURES_FOLDER", - "HOME","DESKTOP","EXIF_ISO","EXIF_EXPOSURE","EXIF_EXPOSURE_BIAS","EXIF_APERTURE","EXIF_FOCUS_DISTANCE", - "EXIF_FOCAL_LENGTH","LONGITUDE","LATITUDE","ELEVATION","LENS","DESCRIPTION","EXIF_CROP"}, widgets = {}, } rename.module_installed = false @@ -72,7 +65,7 @@ rename.event_registered = false local script_data = {} script_data.metadata = { - name = "rename_images", + name = _("rename images"), purpose = _("rename an image file or files"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/rename_images" @@ -99,83 +92,6 @@ local DESKTOP = HOME .. PS .. "Desktop" -- F U N C T I O N S -- - - - - - - - - - - - - - - - - - - - - - - - -local function build_substitution_list(image, sequence, datetime, username, pic_folder, home, desktop) - -- build the argument substitution list from each image - -- local datetime = os.date("*t") - local colorlabels = {} - if image.red then table.insert(colorlabels, "red") end - if image.yellow then table.insert(colorlabels, "yellow") end - if image.green then table.insert(colorlabels, "green") end - if image.blue then table.insert(colorlabels, "blue") end - if image.purple then table.insert(colorlabels, "purple") end - local labels = #colorlabels == 1 and colorlabels[1] or du.join(colorlabels, ",") - local eyear,emon,eday,ehour,emin,esec = string.match(image.exif_datetime_taken, "(%d-):(%d-):(%d-) (%d-):(%d-):(%d-)$") - local replacements = {image.film, - image.path, - df.get_filename(image.filename), - string.upper(df.get_filetype(image.filename)), - image.id,image.duplicate_index, - string.format("%04d", sequence), - datetime.year, - string.format("%02d", datetime.month), - string.format("%02d", datetime.day), - string.format("%02d", datetime.hour), - string.format("%02d", datetime.min), - string.format("%02d", datetime.sec), - eyear, - emon, - eday, - ehour, - emin, - esec, - image.rating, - labels, - image.exif_maker, - image.exif_model, - image.title, - image.creator, - image.publisher, - image.rights, - username, - pic_folder, - home, - desktop, - image.exif_iso, - image.exif_exposure, - image.exif_exposure_bias, - image.exif_aperture, - image.exif_focus_distance, - image.exif_focal_length, - image.longitude, - image.latitude, - image.elevation, - image.exif_lens, - image.description, - image.exif_crop - } - - for i=1,#rename.placeholders,1 do rename.substitutes[rename.placeholders[i]] = replacements[i] end -end - -local function substitute_list(str) - -- replace the substitution variables in a string - for match in string.gmatch(str, "%$%(.-%)") do - local var = string.match(match, "%$%((.-)%)") - if rename.substitutes[var] then - str = string.gsub(str, "%$%("..var.."%)", rename.substitutes[var]) - else - dt.print_error("unrecognized variable " .. var) - dt.print(string.format(_("unknown variable %s, aborting..."), var)) - return -1 - end - end - return str -end - -local function clear_substitute_list() - for i=1,#rename.placeholders,1 do rename.substitutes[rename.placeholders[i]] = nil end -end - local function stop_job(job) job.valid = false end @@ -214,6 +130,7 @@ end local function do_rename(images) if #images > 0 then + local first_image = images[1] local pattern = rename.widgets.pattern.text dt.preferences.write(MODULE_NAME, "pattern", "string", pattern) dt.print_log("pattern is " .. pattern) @@ -224,14 +141,14 @@ local function do_rename(images) for i, image in ipairs(images) do if job.valid then job.percent = i / #images - build_substitution_list(image, i, datetime, USER, PICTURES, HOME, DESKTOP) - local new_name = substitute_list(pattern) + ds.build_substitute_list(image, i, pattern, USER, PICTURES, HOME, DESKTOP) + local new_name = ds.substitute_list(pattern) if new_name == -1 then dt.print(_("unable to do variable substitution, exiting...")) stop_job(job) return end - clear_substitute_list() + ds.clear_substitute_list() local args = {} local path = string.sub(df.get_path(new_name), 1, -2) if string.len(path) == 0 then @@ -256,6 +173,7 @@ local function do_rename(images) stop_job(job) local collect_rules = dt.gui.libs.collect.filter() dt.gui.libs.collect.filter(collect_rules) + dt.gui.views.lighttable.set_image_visible(first_image) dt.print(string.format(_("renamed %d images"), #images)) else -- pattern length dt.print_error("no pattern supplied, returning...") @@ -276,50 +194,8 @@ end -- - - - - - - - - - - - - - - - - - - - - - - - rename.widgets.pattern = dt.new_widget("entry"){ - tooltip = _("$(ROLL_NAME) - film roll name\n") .. - _("$(FILE_FOLDER) - image file folder\n") .. - _("$(FILE_NAME) - image file name\n") .. - _("$(FILE_EXTENSION) - image file extension\n") .. - _("$(ID) - image id\n") .. - _("$(VERSION) - version number\n") .. - _("$(SEQUENCE) - sequence number of selection\n") .. - _("$(YEAR) - current year\n") .. - _("$(MONTH) - current month\n") .. - _("$(DAY) - current day\n") .. - _("$(HOUR) - current hour\n") .. - _("$(MINUTE) - current minute\n") .. - _("$(SECOND) - current second\n") .. - _("$(EXIF_YEAR) - EXIF year\n") .. - _("$(EXIF_MONTH) - EXIF month\n") .. - _("$(EXIF_DAY) - EXIF day\n") .. - _("$(EXIF_HOUR) - EXIF hour\n") .. - _("$(EXIF_MINUTE) - EXIF minute\n") .. - _("$(EXIF_SECOND) - EXIF seconds\n") .. - _("$(EXIF_ISO) - EXIF ISO\n") .. - _("$(EXIF_EXPOSURE) - EXIF exposure\n") .. - _("$(EXIF_EXPOSURE_BIAS) - EXIF exposure bias\n") .. - _("$(EXIF_APERTURE) - EXIF aperture\n") .. - _("$(EXIF_FOCAL_LENGTH) - EXIF focal length\n") .. - _("$(EXIF_FOCUS_DISTANCE) - EXIF focus distance\n") .. - _("$(EXIF_CROP) - EXIF crop\n") .. - _("$(LONGITUDE) - longitude\n") .. - _("$(LATITUDE) - latitude\n") .. - _("$(ELEVATION) - elevation\n") .. - _("$(STARS) - star rating\n") .. - _("$(LABELS) - color labels\n") .. - _("$(MAKER) - camera maker\n") .. - _("$(MODEL) - camera model\n") .. - _("$(LENS) - lens\n") .. - _("$(TITLE) - title from metadata\n") .. - _("$(DESCRIPTION) - description from metadata\n") .. - _("$(CREATOR) - creator from metadata\n") .. - _("$(PUBLISHER) - publisher from metadata\n") .. - _("$(RIGHTS) - rights from metadata\n") .. - _("$(USERNAME) - username\n") .. - _("$(PICTURES_FOLDER) - pictures folder\n") .. - _("$(HOME) - user's home directory\n") .. - _("$(DESKTOP) - desktop directory"), - placeholder = _("enter pattern $(FILE_FOLDER)/$(FILE_NAME)"), + tooltip = ds.get_substitution_tooltip(), + placeholder = _("enter pattern") .. "$(FILE_FOLDER)/$(FILE_NAME)", text = "" } diff --git a/contrib/select_non_existing.lua b/contrib/select_non_existing.lua index f691589a..b47c7cf6 100644 --- a/contrib/select_non_existing.lua +++ b/contrib/select_non_existing.lua @@ -31,8 +31,6 @@ local PS = dt.configuration.running_os == "windows" and "\\" or "/" local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("select_non_existing", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -76,7 +74,7 @@ dt.gui.libs.select.register_selection( local script_data = {} script_data.metadata = { - name = "select_non_existing", + name = _("select non existing"), purpose = _("enable selection of non-existing images"), author = "Dirk Dittmar", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/select_non_existing" diff --git a/contrib/select_untagged.lua b/contrib/select_untagged.lua index 2aef97e8..0ca4b5d2 100644 --- a/contrib/select_untagged.lua +++ b/contrib/select_untagged.lua @@ -24,8 +24,6 @@ local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "select_untagged") -dt.gettext.bindtextdomain("select_untagged", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -35,7 +33,7 @@ end local script_data = {} script_data.metadata = { - name = "select_untagged", + name = _("select untagged"), purpose = _("enable selection of untagged images"), author = "Jannis_V", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/select_untagged" diff --git a/contrib/slideshowMusic.lua b/contrib/slideshowMusic.lua index c5b9d8ce..bed3da6f 100644 --- a/contrib/slideshowMusic.lua +++ b/contrib/slideshowMusic.lua @@ -31,8 +31,6 @@ local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "slideshowMusic") -dt.gettext.bindtextdomain("slideshowMusic", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -42,7 +40,7 @@ end local script_data = {} script_data.metadata = { - name = "slideshowMusic", + name = _("slideshow music"), purpose = _("play music during a slideshow"), author = "Tobias Jakobs", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/slideshowMusic" diff --git a/contrib/transfer_hierarchy.lua b/contrib/transfer_hierarchy.lua index 96b18e44..dd9c2708 100755 --- a/contrib/transfer_hierarchy.lua +++ b/contrib/transfer_hierarchy.lua @@ -76,14 +76,13 @@ local darktable = require("darktable") local dtutils = require("lib/dtutils") local dtutils_file = require("lib/dtutils.file") local dtutils_system = require("lib/dtutils.system") +local gettext = darktable.gettext.gettext local LIB_ID = "transfer_hierarchy" dtutils.check_min_api_version("7.0.0", LIB_ID) -darktable.gettext.bindtextdomain("transfer_hierarchy", darktable.configuration.config_dir .."/lua/locale/") - local function _(msgid) - return darktable.gettext.gettext(msgid) + return gettext(msgid) end -- return data structure for script_manager @@ -91,7 +90,7 @@ end local script_data = {} script_data.metadata = { - name = "transfer_hierarchy", + name = _("transfer hierarchy"), purpose = _("allows the moving or copying of images from one directory tree to another, while preserving the existing hierarchy"), author = "August Schwerdfeger (august@schwerdfeger.name)", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/transfer_hierarchy" @@ -145,7 +144,7 @@ end local function install_module() if not th.module_installed then darktable.register_lib(LIB_ID, - "transfer hierarchy", true, true, { + _("transfer hierarchy"), true, true, { [darktable.gui.views.lighttable] = { "DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 700 } }, th.transfer_widget, nil, nil) th.module_installed = true @@ -352,7 +351,7 @@ th.transfer_widget = darktable.new_widget("box") { }, darktable.new_widget("button") { label = _("copy"), - tooltip = _("Copy all selected images"), + tooltip = _("copy all selected images"), clicked_callback = doCopy } } @@ -406,4 +405,4 @@ script_data.restart = restart script_data.destroy_method = "hide" script_data.show = restart -return script_data \ No newline at end of file +return script_data diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua new file mode 100644 index 00000000..e5562324 --- /dev/null +++ b/contrib/ultrahdr.lua @@ -0,0 +1,1066 @@ +--[[ + + UltraHDR image generation 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 +Generate UltraHDR JPEG images from various combinations of source files (SDR, HDR, gain map). + +https://developer.android.com/media/platform/hdr-image-format + +The images are merged using libultrahdr example application (ultrahdr_app). + +ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT +* 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 binary tool paths +* Use UltraHDR module to generate UltraHDR images from selection + +]] 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" +local gettext = dt.gettext.gettext + +local namespace = "ultrahdr" + +local LOG_LEVEL = log.info + +-- works with darktable API version from 4.8.0 on +du.check_min_api_version("9.3.0", "ultrahdr") + +local function _(msgid) + return gettext(msgid) +end + +local job + +local GUI = { + optionwidgets = { + settings_label = {}, + encoding_variant_combo = {}, + selection_type_combo = {}, + encoding_settings_box = {}, + output_settings_label = {}, + output_settings_box = {}, + output_filepath_label = {}, + output_filepath_widget = {}, + overwrite_on_conflict = {}, + copy_exif = {}, + import_to_darktable = {}, + min_content_boost = {}, + max_content_boost = {}, + hdr_capacity_min = {}, + hdr_capacity_max = {}, + metadata_label = {}, + metadata_box = {}, + edit_executables_button = {}, + executable_path_widget = {}, + quality_widget = {}, + gainmap_downsampling_widget = {}, + target_display_peak_nits_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 = {} + +script_data.metadata = { + 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 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 + +-- 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 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() + log.log_level(level) + return old_log_level +end + +local function restore_log_level(level) + log.log_level(level) +end + +local function generate_metadata_file(settings) + local old_log_level = set_log_level(LOG_LEVEL) + local metadata_file_fmt = [[--maxContentBoost %f +--minContentBoost %f +--gamma 1.0 +--offsetSdr 0.0 +--offsetHdr 0.0 +--hdrCapacityMin %f +--hdrCapacityMax %f]] + + 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 + 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() + restore_log_level(old_log_level) + return filename +end + +local function save_preferences() + local old_log_level = set_log_level(LOG_LEVEL) + 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, "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 + 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 + 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, "target_display_peak_nits", "integer", + (GUI.optionwidgets.target_display_peak_nits_widget.value + 0.5) // 1) + restore_log_level(old_log_level) +end + +local function default_to(value, default) + if value == 0 or value == "" then + return default + end + return value +end + +local function load_preferences() + local old_log_level = set_log_level(LOG_LEVEL) + -- 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.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") + 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) + GUI.optionwidgets.quality_widget.value = default_to(dt.preferences.read(namespace, "quality", "integer"), 95) + 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) + restore_log_level(old_log_level) +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("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 + 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 old_log_level = set_log_level(LOG_LEVEL) + 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) + 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(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 + end + end + 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 + +local function assert_settings_correct(encoding_variant) + local old_log_level = set_log_level(LOG_LEVEL) + local errors = {} + 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") + }, + 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 = { + 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 + }, + quality = GUI.optionwidgets.quality_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. + force_export = true -- if false, will copy source files instead of exporting if the file extension matches the format expectation. + } + + for k, v in pairs(settings.bin) do + if not v then + table.insert(errors, string.format(_("%s binary not found"), k)) + end + end + + if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then + 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 + restore_log_level(old_log_level) + if #errors > 0 then + return nil, errors + end + return settings, nil +end + +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 old_log_level = set_log_level(LOG_LEVEL) + local stacks = {} + local primary = "sdr" + local extra + if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then + extra = "gainmap" + elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then + extra = "hdr" + elseif encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then + extra = nil + elseif encoding_variant == ENCODING_VARIANT_HDR_ONLY then + extra = nil + primary = "hdr" + end + + local tags = nil + -- 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 and tag.name == extra 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 + 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 + if extra and (is_extra or stacks[key][primary]) then + -- Don't overwrite existing entries + 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][primary] then + stacks[key][primary] = v + end + end + end + -- remove invalid stacks + local count = 0 + for k, v in pairs(stacks) do + if extra then + if not v[primary] or not v[extra] then + stacks[k] = nil + else + 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 + end + end + if stacks[k] then + count = count + 1 + end + end + restore_log_level(old_log_level) + return stacks, count +end + +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 old_log_level = set_log_level(LOG_LEVEL) + local total_substeps + local substep = 0 + local best_source_image + 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 + 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 + + 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" + 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_profile(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 = exporter:write_image(src_image, dest) + 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 + return ok + end + return true + end + + function cleanup() + if settings.skip_cleanup then + return false + end + 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 = 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") + table.insert(remove_files, sdr) + ok = copy_or_export(images["sdr"], sdr, "jpeg", DT_COLORSPACE_DISPLAY_P3, { + quality = settings.quality + }) + if not ok then + table.insert(errors, string.format(_("Error exporting %s to %s"), images["sdr"].filename, "jpeg")) + return cleanup(), 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") + table.insert(remove_files, gainmap) + ok = copy_or_export(images["gainmap"], gainmap, "jpeg", DT_COLORSPACE_DISPLAY_P3, { + quality = settings.quality + }) + if not ok then + table.insert(errors, string.format(_("Error exporting %s to %s"), images["gainmap"].filename, "jpeg")) + return cleanup(), errors + end + end + log.msg(log.debug, string.format("Exported files: %s, %s", sdr, gainmap)) + update_job_progress() + -- Strip EXIFs + 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 + 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" + table.insert(remove_files, 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 + 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. + 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 + 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) .. + ".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 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 + table.insert(errors, string.format(_("Error exporting %s to %s"), images["sdr"].filename, "png")) + 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_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 + 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 + 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 + -- 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)) + return cleanup(), errors + end + update_job_progress() + 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 + 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(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_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_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 + 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)) + return cleanup(), errors + end + 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 + cleanup() + update_job_progress() + 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 + +local function main() + local old_log_level = set_log_level(LOG_LEVEL) + save_preferences() + + 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 + 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) + 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(_("Generating UltraHDR images failed:\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) + end + -- stop job and remove progress_bar from ui, but only if not alreay canceled + if (job.valid) then + job.valid = false + end + + 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 + +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_filepath_label = dt.new_widget("label") { + label = _("file path pattern"), + tooltip = ds.get_substitution_tooltip() +} + +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") { + 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"), + 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.output_filepath_label, + GUI.optionwidgets.output_filepath_widget, + GUI.optionwidgets.overwrite_on_conflict, + GUI.optionwidgets.import_to_darktable, + GUI.optionwidgets.copy_exif +} + +GUI.optionwidgets.metadata_label = dt.new_widget("label") { + label = _("gain map metadata") +} + +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.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_box = dt.new_widget("box") { + orientation = "vertical", + 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") { + 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 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. + +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 + 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 + _("SDR + HDR"), -- ENCODING_VARIANT_SDR_AND_HDR + _("SDR only"), -- ENCODING_VARIANT_SDR_AUTO_GAINMAP + _("HDR only") -- ENCODING_VARIANT_HDR_ONLY +} + +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 resolution. +]]), _("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.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.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)'), + 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.target_display_peak_nits_widget, + GUI.optionwidgets.metadata_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"), + 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.gui.libs[namespace].visible = false +end + +local function restart() + dt.gui.libs[namespace].visible = true +end + +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) + flags.event_registered = true -- keep track of whether we have an event handler installed + end +end + +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = restart + +return script_data diff --git a/contrib/video_ffmpeg.lua b/contrib/video_ffmpeg.lua index 4203188e..f0713fa6 100644 --- a/contrib/video_ffmpeg.lua +++ b/contrib/video_ffmpeg.lua @@ -41,8 +41,6 @@ local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "video_ffmpeg") -dt.gettext.bindtextdomain("video_ffmpeg", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -52,7 +50,7 @@ end local script_data = {} script_data.metadata = { - name = "video_ffmpeg", + name = _("video ffmpeg"), purpose = _("timelapse video plugin based on ffmpeg"), author = "Dominik Markiewicz", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contib/video_ffmpeg" @@ -248,8 +246,8 @@ local function string_pref_write(name, widget_attribute) end local framerates_selector = dt.new_widget("combobox"){ - label = _("framerate"), - tooltip = _("select framerate of output video"), + label = _("frame rate"), + tooltip = _("select frame rate of output video"), value = combobox_pref_read("framerate", framerates), changed_callback = combobox_pref_write("framerate"), table.unpack(framerates) @@ -418,7 +416,7 @@ local function export(extra_data) local ffmpeg_path = df.check_if_bin_exists("ffmpeg") if not ffmpeg_path then dt.print_error("ffmpeg not found") - dt.print("ERROR - ffmpeg not found") + dt.print(_("ERROR - ffmpeg not found")) return end local dir = extra_data["tmp_dir"] @@ -450,7 +448,7 @@ local function finalize_export(storage, images_table, extra_data) dt.print_error(filename, file.filename) df.file_move(filename, tmp_dir .. PS .. i .. extra_data["img_ext"]) end - dt.print("Start video building...") + dt.print(_("start video building...")) local result, path = export(extra_data) if result ~= 0 then dt.print(_("ERROR: cannot build image, see console for more info")) diff --git a/data/icons/blank20.png b/data/icons/blank20.png new file mode 100644 index 00000000..81ed5157 Binary files /dev/null and b/data/icons/blank20.png differ diff --git a/data/icons/path20.png b/data/icons/path20.png new file mode 100644 index 00000000..1021d864 Binary files /dev/null and b/data/icons/path20.png differ diff --git a/examples/api_version.lua b/examples/api_version.lua index d54b7883..bf8a1c17 100644 --- a/examples/api_version.lua +++ b/examples/api_version.lua @@ -27,8 +27,6 @@ local dt = require "darktable" local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("api_version", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -41,7 +39,7 @@ end local result = dt.configuration.api_version_string dt.print_log("API Version: " .. result) -dt.print(string.format(_("API version: %s"), result)) +dt.print("API " .. _("version") .. ": " .. result) -- set the destroy routine so that script_manager can call it when -- it's time to destroy the script and then return the data to @@ -49,7 +47,7 @@ dt.print(string.format(_("API version: %s"), result)) local script_data = {} script_data.metadata = { - name = "api_version", + name = _("APIversion"), purpose = _("display api_version example"), author = "Tobias Jakobs", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/api_version" diff --git a/examples/darkroom_demo.lua b/examples/darkroom_demo.lua index 802df749..b76b552e 100644 --- a/examples/darkroom_demo.lua +++ b/examples/darkroom_demo.lua @@ -60,8 +60,6 @@ local PS = dt.configuration.running_os == "windows" and "\\" or "/" -- - - - - - - - - - - - - - - - - - - - - - - - local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("darkroom_demo", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -93,7 +91,7 @@ dt.gui.current_view(dt.gui.views.darkroom) local max_images = 10 -dt.print(_("showing images, with a pause in between each")) +dt.print(_("showing images, with a pause between each")) sleep(1500) -- display first 10 images of collection pausing for a second between each @@ -119,7 +117,7 @@ dt.gui.current_view(current_view) local script_data = {} script_data.metadata = { - name = "darkroom_demo", + name = _("darkroom demo"), purpose = _("example demonstrating how to control image display in darkroom mode"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/darkroom_demo" diff --git a/examples/gettextExample.lua b/examples/gettextExample.lua index 20d5db43..42e1a479 100644 --- a/examples/gettextExample.lua +++ b/examples/gettextExample.lua @@ -69,8 +69,6 @@ dt.print_error("Hello World!") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("gettextExample", dt.configuration.config_dir .."/lua/locale/") - -- Translate a string using the darktable textdomain dt.print_error(gettext("image")) @@ -88,7 +86,7 @@ dt.print_error(_("hello world!")) local script_data = {} script_data.metadata = { - name = "gettextExample", + name = _("gettext example"), purpose = _("example of how translations works"), author = "Tobias Jakobs", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/gettextExample" diff --git a/examples/gui_action.lua b/examples/gui_action.lua index 3bff94b6..9fee3619 100644 --- a/examples/gui_action.lua +++ b/examples/gui_action.lua @@ -5,7 +5,6 @@ local NaN = 0/0 local wg = {} local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("gui_action", dt.configuration.config_dir .."/lua/locale/") local function _(msgid) return gettext(msgid) @@ -16,7 +15,7 @@ end local script_data = {} script_data.metadata = { - name = "gui_action", + name = _("gui action"), purpose = _("example of how to use darktable.gui.action() calls"), author = "Diederik ter Rahe", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/gui_action" @@ -29,37 +28,37 @@ script_data.show = nil -- only required for libs since the destroy_method only h wg.action = dt.new_widget("entry"){ text = "lib/filter/view", - placeholder = "action path", - tooltip = "enter the full path of an action, for example 'lib/filter/view'" + placeholder = _("action path"), + tooltip = _("enter the full path of an action, for example 'lib/filter/view'") } wg.instance = dt.new_widget("combobox"){ - label = "instance", - tooltip = "the instance of an image processing module to execute action on", + label = _("instance"), + tooltip = _("the instance of an image processing module to execute action on"), "0", "+1", "-1", "+2", "-2", "+3", "-3", "+4", "-4", "+5", "-5", "+6", "-6", "+7", "-7", "+8", "-8", "+9", "-9" } wg.element = dt.new_widget("entry"){ text = "", - placeholder = "action element", - tooltip = "enter the element of an action, for example 'selection', or leave empty for default" + placeholder = _("action element"), + tooltip = _("enter the element of an action, for example 'selection', or leave empty for default") } wg.effect = dt.new_widget("entry"){ text = "next", - placeholder = "action effect", - tooltip = "enter the effect of an action, for example 'next', or leave empty for default" + placeholder = _("action effect"), + tooltip = _("enter the effect of an action, for example 'next', or leave empty for default") } wg.speed = dt.new_widget("entry"){ text = "1", - placeholder = "action speed", - tooltip = "enter the speed to use in action execution, or leave empty to only read state" + placeholder = _("action speed"), + tooltip = _("enter the speed to use in action execution, or leave empty to only read state") } wg.check = dt.new_widget("check_button"){ - label = 'perform action', - tooltip = 'perform action or only read return', + label = _('perform action'), + tooltip = _('perform action or only read return'), clicked_callback = function() wg.speed.sensitive = wg.check.value end, @@ -73,7 +72,7 @@ wg.return_value = dt.new_widget("entry"){ dt.register_lib( "execute_action", -- Module name - "execute gui actions", -- name + _("execute gui actions"), -- name true, -- expandable false, -- resetable {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 100}, @@ -85,37 +84,37 @@ dt.register_lib( dt.new_widget("box") { orientation = "horizontal", - dt.new_widget("label"){label = "action path", halign = "start"}, + dt.new_widget("label"){label = _("action path"), halign = "start"}, wg.action }, wg.instance, dt.new_widget("box") { orientation = "horizontal", - dt.new_widget("label"){label = "element", halign = "start"}, + dt.new_widget("label"){label = _("element"), halign = "start"}, wg.element }, dt.new_widget("box") { orientation = "horizontal", - dt.new_widget("label"){label = "effect", halign = "start"}, + dt.new_widget("label"){label = _("effect"), halign = "start"}, wg.effect }, wg.check, dt.new_widget("box") { orientation = "horizontal", - dt.new_widget("label"){label = "speed", halign = "start"}, + dt.new_widget("label"){label = _("speed"), halign = "start"}, wg.speed }, dt.new_widget("button") { - label = "execute action", - tooltip = "execute the action specified in the fields above", + label = _("execute action"), + tooltip = _("execute the action specified in the fields above"), clicked_callback = function(_) local sp = NaN if wg.check.value then sp = wg.speed.text end - wg.return_value.text = dt.gui.action(wg.action.text, wg.instance.value, wg.element.text, wg.effect.text, sp) + wg.return_value.text = dt.gui.action(wg.action.text, tonumber(wg.instance.value), wg.element.text, wg.effect.text, tonumber(sp)) end }, dt.new_widget("box") diff --git a/examples/hello_world.lua b/examples/hello_world.lua index e64c4f9a..6e04efc4 100644 --- a/examples/hello_world.lua +++ b/examples/hello_world.lua @@ -35,8 +35,6 @@ du.check_min_api_version("2.0.0", "hello_world") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("hello_world", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -55,7 +53,7 @@ dt.print(_("hello, world")) local script_data = {} script_data.metadata = { - name = "hello_world", + name = _("hello world"), purpose = _("example of how to print a message to the screen"), author = "Tobias Ellinghaus", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/hello_world" diff --git a/examples/lighttable_demo.lua b/examples/lighttable_demo.lua index d897ff8a..e28454cd 100644 --- a/examples/lighttable_demo.lua +++ b/examples/lighttable_demo.lua @@ -218,7 +218,7 @@ current_sort_order = dt.gui.libs.filter.sort_order(current_sort_order) local script_data = {} script_data.metadata = { - name = "lighttable_demo", + name = _("lighttable demo"), purpose = _("example demonstrating how to control lighttable display modes"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/lighttable_demo" diff --git a/examples/moduleExample.lua b/examples/moduleExample.lua index edf4815a..89c56bc2 100644 --- a/examples/moduleExample.lua +++ b/examples/moduleExample.lua @@ -38,8 +38,6 @@ du.check_min_api_version("7.0.0", "moduleExample") -- https://www.darktable.org/lua-api/index.html#darktable_gettext local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("moduleExample", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -49,7 +47,7 @@ end local script_data = {} script_data.metadata = { - name = "moduleExample", + name = ("module example"), purpose = _("example of how to create a lighttable module"), author = "Tobias Jakobs", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/moduleExample" @@ -77,7 +75,7 @@ local function install_module() -- https://www.darktable.org/lua-api/index.html#darktable_register_lib dt.register_lib( "exampleModule", -- Module name - "exampleModule", -- name + _("example module"), -- name true, -- expandable false, -- resetable {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers @@ -87,7 +85,7 @@ local function install_module() orientation = "vertical", dt.new_widget("button") { - label = _("my button"), + label = _("my ") .. "button", clicked_callback = function (_) dt.print(_("button clicked")) end @@ -112,10 +110,10 @@ local function restart() end -- https://www.darktable.org/lua-api/types_lua_check_button.html -local check_button = dt.new_widget("check_button"){label = _("my check_button"), value = true} +local check_button = dt.new_widget("check_button"){label = _("my ") .. "check_button", value = true} -- https://www.darktable.org/lua-api/types_lua_combobox.html -local combobox = dt.new_widget("combobox"){label = _("my combobox"), value = 2, "8", "16", "32"} +local combobox = dt.new_widget("combobox"){label = _("my ") .. "combobox", value = 2, "8", "16", "32"} -- https://www.darktable.org/lua-api/types_lua_entry.html local entry = dt.new_widget("entry") @@ -131,14 +129,14 @@ local entry = dt.new_widget("entry") -- https://www.darktable.org/lua-api/types_lua_file_chooser_button.html local file_chooser_button = dt.new_widget("file_chooser_button") { - title = _("my file_chooser_button"), -- The title of the window when choosing a file + title = _("my ") .. "file_chooser_button", -- The title of the window when choosing a file value = "", -- The currently selected file is_directory = false -- True if the file chooser button only allows directories to be selecte } -- https://www.darktable.org/lua-api/types_lua_label.html local label = dt.new_widget("label") -label.label = _("my label") -- This is an alternative way to the "{}" syntax to set a property +label.label = _("my ") .. "label" -- This is an alternative way to the "{}" syntax to set a property -- https://www.darktable.org/lua-api/types_lua_separator.html local separator = dt.new_widget("separator"){} @@ -146,7 +144,7 @@ local separator = dt.new_widget("separator"){} -- https://www.darktable.org/lua-api/types_lua_slider.html local slider = dt.new_widget("slider") { - label = _("my slider"), + label = _("my ") .. "slider", soft_min = 10, -- The soft minimum value for the slider, the slider can't go beyond this point soft_max = 100, -- The soft maximum value for the slider, the slider can't go beyond this point hard_min = 0, -- The hard minimum value for the slider, the user can't manually enter a value beyond this point diff --git a/examples/multi_os.lua b/examples/multi_os.lua index f27cc9ea..21dc0cb1 100644 --- a/examples/multi_os.lua +++ b/examples/multi_os.lua @@ -72,8 +72,6 @@ local dtsys = require "lib/dtutils.system" -- system utilities local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("multi_os", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -215,7 +213,7 @@ end if dt.configuration.running_os ~= "linux" then local executable = "ufraw-batch" local ufraw_batch_path_widget = dt.new_widget("file_chooser_button"){ - title = _("select ufraw-batch[.exe] executable"), + title = string.format(_("select %s executable"), "ufraw-batch[.exe]"), value = df.get_executable_path_preference(executable), is_directory = false, changed_callback = function(self) @@ -226,8 +224,8 @@ if dt.configuration.running_os ~= "linux" then } dt.preferences.register("executable_paths", "ufraw-batch", -- name "file", -- type - _('multi_os: ufraw-batch location'), -- label - _('Installed location of ufraw-batch, requires restart to take effect.'), -- tooltip + 'multi_os: ufraw-batch ' .. _('location'), -- label + _('installed location of ufraw-batch, requires restart to take effect.'), -- tooltip "ufraw-batch", -- default ufraw_batch_path_widget ) @@ -262,7 +260,7 @@ dt.register_event( local script_data = {} script_data.metadata = { - name = "multi_os", + name = _("multi OS"), purpose = _("example module thet runs on different operating systems"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/multi_os" diff --git a/examples/panels_demo.lua b/examples/panels_demo.lua index 36e31e3c..ac9e3de4 100644 --- a/examples/panels_demo.lua +++ b/examples/panels_demo.lua @@ -61,8 +61,6 @@ local PS = dt.configuration.running_os == "windows" and "\\" or "/" -- - - - - - - - - - - - - - - - - - - - - - - - local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("panels_demo", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -95,11 +93,11 @@ dt.gui.panel_show_all() -- hide center_top, center_bottom, left, top, right, bottom in order -dt.print(_("hiding all panels, one at a tme")) +dt.print(_("hiding all panels, one at a time")) sleep(1500) for i = 1, #panels do - dt.print(_("hiding " .. panels[i])) + dt.print(string.format(_("hiding %s"), panels[i])) dt.gui.panel_hide(panels[i]) sleep(1500) end @@ -110,7 +108,7 @@ end sleep(1500) for i = #panels, 1, -1 do - dt.print(_("showing " .. panels[i])) + dt.print(string.format(_("showing %s"), panels[i])) dt.gui.panel_show(panels[i]) sleep(1500) end @@ -148,7 +146,7 @@ end local script_data = {} script_data.metadata = { - name = "panels_demo", + name = _("panels demo"), purpose = _("example demonstrating how to contol panel visibility"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/panels_demo" diff --git a/examples/preferenceExamples.lua b/examples/preferenceExamples.lua index e3721ca6..84425744 100644 --- a/examples/preferenceExamples.lua +++ b/examples/preferenceExamples.lua @@ -28,8 +28,6 @@ local du = require "lib/dtutils" local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("preferenceExamples", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -37,7 +35,7 @@ end local script_data = {} script_data.metadata = { - name = "preferenceExamples", + name = _("preference examples"), purpose = _("example to show the different preference types that are possible"), author = "Tobias Jakobs", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/preferenceExamples" @@ -49,22 +47,22 @@ du.check_min_api_version("2.0.1", "preferenceExamples") dt.preferences.register("preferenceExamples", -- script: This is a string used to avoid name collision in preferences (i.e namespace). Set it to something unique, usually the name of the script handling the preference. "preferenceExamplesString", -- name "string", -- type - _("example string"), -- label - _("example string tooltip"), -- tooltip + _("example") .. " string", -- label + _("example") .. " string " .. _("tooltip"), -- tooltip "String") -- default dt.preferences.register("preferenceExamples", -- script: This is a string used to avoid name collision in preferences (i.e namespace). Set it to something unique, usually the name of the script handling the preference. "preferenceExamplesBool", -- name "bool", -- type - _("example boolean"), -- label - _("example boolean tooltip"), -- tooltip + _("example") .. " boolean", -- label + _("example") .. " boolean " .. _("tooltip"), -- tooltip true) -- default dt.preferences.register("preferenceExamples", -- script: This is a string used to avoid name collision in preferences (i.e namespace). Set it to something unique, usually the name of the script handling the preference. "preferenceExamplesInteger", -- name "integer", -- type - _("example integer"), -- label - _("example integer tooltip"), -- tooltip + _("example") .. " integer", -- label + _("example") .. " integer " .. _("tooltip"), -- tooltip 2, -- default 1, -- min 99) -- max @@ -72,8 +70,8 @@ dt.preferences.register("preferenceExamples", -- script: This is a string dt.preferences.register("preferenceExamples", -- script: This is a string used to avoid name collision in preferences (i.e namespace). Set it to something unique, usually the name of the script handling the preference. "preferenceExamplesFloat", -- name "float", -- type - _("example float"), -- label - _("example float tooltip"), -- tooltip + _("example") .. " float", -- label + _("example") .. " float " .. _("tooltip"), -- tooltip 1.3, -- default 1, -- min 99, -- max @@ -82,22 +80,22 @@ dt.preferences.register("preferenceExamples", -- script: This is a string dt.preferences.register("preferenceExamples", -- script: This is a string used to avoid name collision in preferences (i.e namespace). Set it to something unique, usually the name of the script handling the preference. "preferenceExamplesFile", -- name "file", -- type - _("example file"), -- label - _("example file tooltip"), -- tooltip + _("example") .. " file", -- label + _("example") .. " file " .. _("tooltip"), -- tooltip "") -- default dt.preferences.register("preferenceExamples", -- script: This is a string used to avoid name collision in preferences (i.e namespace). Set it to something unique, usually the name of the script handling the preference. "preferenceExamplesDirectory", -- name "directory", -- type - _("example directory"), -- label - _("example directory tooltip"), -- tooltip + _("example") .. " directory", -- label + _("example") .. " directory " .. _("tooltip"), -- tooltip "") -- default dt.preferences.register("preferenceExamples", -- script: This is a string used to avoid name collision in preferences (i.e namespace). Set it to something unique, usually the name of the script handling the preference. "preferenceExamplesEnum", -- name "enum", -- type - _("example enum"), -- label - _("example enum tooltip"), -- tooltip + _("example") .. " enum", -- label + _("example") .. " enum " .. _("tooltip"), -- tooltip "Enum 1", -- default "Enum 1", "Enum 2") -- values diff --git a/examples/printExamples.lua b/examples/printExamples.lua index 23614c5b..721138cd 100644 --- a/examples/printExamples.lua +++ b/examples/printExamples.lua @@ -28,8 +28,6 @@ du.check_min_api_version("5.0.0", "printExamples") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("printExamples", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -60,7 +58,7 @@ dt.print_log("print log") local script_data = {} script_data.metadata = { - name = "printExamples", + name = _("print examples"), purpose = _("example showing the different types of printing messages"), author = "Tobias Jakobs", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/printExamples" diff --git a/examples/running_os.lua b/examples/running_os.lua index 1e61cb84..69741288 100644 --- a/examples/running_os.lua +++ b/examples/running_os.lua @@ -34,8 +34,6 @@ du.check_min_api_version("5.0.0", "running_os") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("running_os", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -54,7 +52,7 @@ dt.print(string.format(_("you are running: %s"), dt.configuration.running_os)) local script_data = {} script_data.metadata = { - name = "running_os", + name = _("running OS"), purpose = _("example of how to determine the operating system being used"), author = "Tobias Jakobs", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/running_os" diff --git a/examples/x-touch.lua b/examples/x-touch.lua index dd8dd2e9..7da3d54c 100644 --- a/examples/x-touch.lua +++ b/examples/x-touch.lua @@ -48,6 +48,9 @@ midi:A-1=iop/colorzones;focus midi:A#-1=iop/toneequal;focus midi:B-1=iop/colorbalancergb;focus midi:C0=iop/channelmixerrgb;focus +midi:C#0=iop/colorequal;focus +midi:D0=iop/colorequal/page;previous +midi:D#0=iop/colorequal/page;next ]] local dt = require "darktable" @@ -56,7 +59,6 @@ local du = require "lib/dtutils" du.check_min_api_version("9.2.0", "x-touch") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("gui_action", dt.configuration.config_dir .."/lua/locale/") local function _(msgid) return gettext(msgid) @@ -67,7 +69,7 @@ end local script_data = {} script_data.metadata = { - name = "x-touch", + name = _("x-touch"), purpose = _("example of how to control an x-touch midi device"), author = "Diederik ter Rahe", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/x-touch" @@ -120,6 +122,18 @@ for k = 1,8 do "magenta" } element = e[k] + -- try if colorequalizer module is focused; if so select element of graph + elseif dt.gui.action("iop/colorequal", "focus") ~= 0 then + local e = { "red", + "orange", + "yellow", + "green", + "cyan", + "blue", + "lavender", + "magenta" } + which = "iop/colorequal/graph" + element = e[k] -- if the sigmoid rgb primaries is focused, -- check sliders diff --git a/lib/dtutils.lua b/lib/dtutils.lua index c17ce334..e6595865 100644 --- a/lib/dtutils.lua +++ b/lib/dtutils.lua @@ -61,7 +61,7 @@ dtutils.libdoc.functions["check_min_api_version"] = { function dtutils.check_min_api_version(min_api, script_name) local current_api = dt.configuration.api_version_string - if min_api > current_api then + if dtutils.compare_api_versions(min_api, current_api) > 0 then dt.print_error("This application is written for lua api version " .. min_api .. " or later.") dt.print_error("The current lua api version is " .. current_api) dt.print("ERROR: " .. script_name .. " failed to load. Lua API version " .. min_api .. " or later required.") @@ -82,8 +82,7 @@ dtutils.libdoc.functions["check_max_api_version"] = { run against the current api version. This function is used when a part of the Lua API that the script relies on is removed. If the maximum api version is not met, then an error message is printed saying the script_name failed to load, then an error is thrown causing the - program to stop executing. - + program to stop executing.]], Return_Value = [[result - true if the maximum api version is available, false if not.]], Limitations = [[When using the default handler on a script being executed from the luarc file, the error thrown will stop the luarc file from executing any remaining statements. This limitation does not apply to script_manger.]], @@ -97,7 +96,7 @@ dtutils.libdoc.functions["check_max_api_version"] = { function dtutils.check_max_api_version(max_api, script_name) local current_api = dt.configuration.api_version_string - if current_api > max_api then + if dtutils.compare_api_versions(current_api, max_api) > 0 then dt.print_error("This application is written for lua api version " .. max_api .. " or earlier.") dt.print_error("The current lua api version is " .. current_api) dt.print("ERROR: " .. script_name .. " failed to load. Lua API version " .. max_api .. " or earlier required.") @@ -375,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 = [[]], @@ -392,5 +391,77 @@ 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 + +dtutils.libdoc.functions["compare_api_versions"] = { + Name = [[compare_api_versions]], + Synopsis = [[compare two API version strings]], + Usage = [[local du = require "lib/dtutils" + + local result = du.compare_api_versions(version1, version2) + version1 - string - the first version string to compare (example: "5.0.0") + version2 - string - the second version string to compare (example: "5.1.0")]], + Description = [[compare_api_versions compares two version strings and returns 1 if version1 is greater, + -1 if version2 is greater, and 0 if they are equal.]], + Return_Value = [[result - 1 if version1 is greater, -1 if version2 is greater, 0 if they are equal.]], + Limitations = [[]], + Example = [[compare_api_versions("5.0.0", "5.1.0") returns -1]], + See_Also = [[]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +function dtutils.compare_api_versions(version1, version2) + local v1 = {} + for num in version1:gmatch("%d+") do table.insert(v1, tonumber(num)) end + local v2 = {} + for num in version2:gmatch("%d+") do table.insert(v2, tonumber(num)) end + + for i = 1, math.max(#v1, #v2) do + local num1 = v1[i] or 0 + local num2 = v2[i] or 0 + if num1 > num2 then + return 1 + elseif num1 < num2 then + return -1 + end + end + return 0 +end return dtutils diff --git a/lib/dtutils/file.lua b/lib/dtutils/file.lua index cd898e71..fc2ee9ef 100644 --- a/lib/dtutils/file.lua +++ b/lib/dtutils/file.lua @@ -24,15 +24,12 @@ dtutils_file.libdoc = { functions = {} } -local gettext = dt.gettext +local gettext = dt.gettext.gettext du.check_min_api_version("5.0.0", "dtutils.file") --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("dtutils.file",dt.configuration.config_dir.."/lua/locale/") - local function _(msgid) - return gettext.dgettext("dtutils.file", msgid) + return gettext(msgid) end --[[ @@ -522,9 +519,9 @@ function dtutils_file.file_copy(fromFile, toFile) local result = nil -- if cp exists, use it if dt.configuration.running_os == "windows" then - result = os.execute('copy "' .. fromFile .. '" "' .. toFile .. '"') + result = os.execute('copy ' .. dtutils_file.sanitize_filename(fromFile) .. ' ' .. dtutils_file.sanitize_filename(toFile)) elseif dtutils_file.check_if_bin_exists("cp") then - result = os.execute("cp '" .. fromFile .. "' '" .. toFile .. "'") + result = os.execute("cp " .. dtutils_file.sanitize_filename(fromFile) .. ' ' .. dtutils_file.sanitize_filename(toFile)) end -- if cp was not present, or if cp failed, then a pure lua solution @@ -575,7 +572,7 @@ function dtutils_file.file_move(fromFile, toFile) if not success then -- an error occurred, so let's try using the operating system function if dtutils_file.check_if_bin_exists("mv") then - success = os.execute("mv '" .. fromFile .. "' '" .. toFile .. "'") + success = os.execute("mv " .. dtutils_file.sanitize_filename(fromFile) .. ' ' .. dtutils_file.sanitize_filename(toFile)) end -- if the mv didn't exist or succeed, then... if not success then @@ -815,7 +812,7 @@ dtutils_file.libdoc.functions["mkdir"] = { function dtutils_file.mkdir(path) if not dtutils_file.check_if_file_exists(path) then local mkdir_cmd = dt.configuration.running_os == "windows" and "mkdir" or "mkdir -p" - return dsys.external_command(mkdir_cmd.." "..path) + return dsys.external_command(mkdir_cmd.." "..dtutils_file.sanitize_filename(path)) else return 0 end @@ -840,7 +837,7 @@ dtutils_file.libdoc.functions["rmdir"] = { function dtutils_file.rmdir(path) local rm_cmd = dt.configuration.running_os == "windows" and "rmdir /S /Q" or "rm -r" - return dsys.external_command(rm_cmd.." "..path) + return dsys.external_command(rm_cmd.." "..dtutils_file.sanitize_filename(path)) end dtutils_file.libdoc.functions["create_tmp_file"] = { @@ -861,9 +858,6 @@ dtutils_file.libdoc.functions["create_tmp_file"] = { function dtutils_file.create_tmp_file() local tmp_file = os.tmpname() - if dt.configuration.running_os == "windows" then - tmp_file = dt.configuration.tmp_dir .. tmp_file -- windows os.tmpname() defaults to root directory - end local f = io.open(tmp_file, "w") if not f then diff --git a/lib/dtutils/log.lua b/lib/dtutils/log.lua index 6910c8eb..28dce69f 100644 --- a/lib/dtutils/log.lua +++ b/lib/dtutils/log.lua @@ -199,7 +199,7 @@ function dtutils_log.msg(level, ...) table.remove(args, 1) end local log_msg = level.label - if level.engine ~= dt_screen and call_level ~= 0 then + if level.engine ~= dt_print and call_level ~= 0 then log_msg = log_msg .. dtutils_log.caller(call_level, level.caller_info) .. " " elseif log_msg:len() > 2 then log_msg = log_msg .. " " diff --git a/lib/dtutils/string.lua b/lib/dtutils/string.lua index f7159a2f..48221f19 100644 --- a/lib/dtutils/string.lua +++ b/lib/dtutils/string.lua @@ -1,6 +1,17 @@ local dtutils_string = {} local dt = require "darktable" +local du = require "lib/dtutils" +local log = require "lib/dtutils.log" +local gettext = dt.gettext.gettext + +local DEFAULT_LOG_LEVEL = log.error + +local function _(msg) + return gettext(msg) +end + +dtutils_string.log_level = DEFAULT_LOG_LEVEL dtutils_string.libdoc = { Name = [[dtutils.string]], @@ -48,6 +59,8 @@ dtutils_string.libdoc.functions["strip_accents"] = { } function dtutils_string.strip_accents( str ) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) local tableAccents = {} tableAccents["à"] = "a" tableAccents["á"] = "a" @@ -111,6 +124,7 @@ function dtutils_string.strip_accents( str ) end end + log.log_level(old_log_level) return normalizedString end @@ -136,6 +150,8 @@ dtutils_string.libdoc.functions["escape_xml_characters"] = { -- Keep & first, otherwise it will double escape other characters function dtutils_string.escape_xml_characters( str ) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) str = string.gsub(str,"&", "&") str = string.gsub(str,"\"", """) @@ -143,6 +159,7 @@ function dtutils_string.escape_xml_characters( str ) str = string.gsub(str,"<", "<") str = string.gsub(str,">", ">") + log.log_level(old_log_level) return str end @@ -165,11 +182,14 @@ dtutils_string.libdoc.functions["urlencode"] = { } function dtutils_string.urlencode(str) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) if (str) then str = string.gsub (str, "\n", "\r\n") str = string.gsub (str, "([^%w ])", function (c) return string.format ("%%%02X", string.byte(c)) end) str = string.gsub (str, " ", "+") end + log.log_level(old_log_level) return str end @@ -192,38 +212,52 @@ dtutils_string.libdoc.functions["is_not_sanitized"] = { } local function _is_not_sanitized_posix(str) - -- A sanitized string must be quoted. - if not string.match(str, "^'.*'$") then - return true + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + -- A sanitized string must be quoted. + if not string.match(str, "^'.*'$") then + log.log_level(old_log_level) + return true -- A quoted string containing no quote characters within is sanitized. - elseif string.match(str, "^'[^']*'$") then - return false - end + elseif string.match(str, "^'[^']*'$") then + log.log_level(old_log_level) + return false + end - -- Any quote characters within a sanitized string must be properly - -- escaped. - local quotesStripped = string.sub(str, 2, -2) - local escapedQuotesRemoved = string.gsub(quotesStripped, "'\\''", "") - if string.find(escapedQuotesRemoved, "'") then - return true - else - return false - end + -- Any quote characters within a sanitized string must be properly + -- escaped. + local quotesStripped = string.sub(str, 2, -2) + local escapedQuotesRemoved = string.gsub(quotesStripped, "'\\''", "") + if string.find(escapedQuotesRemoved, "'") then + log.log_level(old_log_level) + return true + else + log.log_level(old_log_level) + return false + end end local function _is_not_sanitized_windows(str) - if not string.match(str, "^\".*\"$") then - return true - else - return false - end + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + if not string.match(str, "^\".*\"$") then + log.log_level(old_log_level) + return true + else + log.log_level(old_log_level) + return false + end end function dtutils_string.is_not_sanitized(str) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) if dt.configuration.running_os == "windows" then - return _is_not_sanitized_windows(str) + log.log_level(old_log_level) + return _is_not_sanitized_windows(str) else - return _is_not_sanitized_posix(str) + log.log_level(old_log_level) + return _is_not_sanitized_posix(str) end end @@ -246,27 +280,63 @@ dtutils_string.libdoc.functions["sanitize"] = { } local function _sanitize_posix(str) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) if _is_not_sanitized_posix(str) then - return "'" .. string.gsub(str, "'", "'\\''") .. "'" + log.log_level(old_log_level) + return "'" .. string.gsub(str, "'", "'\\''") .. "'" else - return str + log.log_level(old_log_level) + return str end end local function _sanitize_windows(str) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) if _is_not_sanitized_windows(str) then - return "\"" .. string.gsub(str, "\"", "\"^\"\"") .. "\"" + log.log_level(old_log_level) + return "\"" .. string.gsub(str, "\"", "\"^\"\"") .. "\"" else - return str + log.log_level(old_log_level) + return str end end -function dtutils_string.sanitize(str) +local function _should_be_sanitized(str) + local old_log_level = log.log_level() + local result = false + local UNSAFE_POSIX_FILENAME_CHARS = "[^%w/._%-]+" + local UNSAFE_WIN_FILENAME_CHARS = "[^%w\\._%-:]+" + + local pattern = UNSAFE_POSIX_FILENAME_CHARS if dt.configuration.running_os == "windows" then - return _sanitize_windows(str) + pattern = UNSAFE_WIN_FILENAME_CHARS + end + + log.log_level(dtutils_string.log_level) + if string.match(str, pattern) then + result = true + end + log.log_level(old_log_level) + return result +end + +function dtutils_string.sanitize(str) + local old_log_level = log.log_level() + local sanitized_str = nil + log.log_level(dtutils_string.log_level) + if _should_be_sanitized(str) then + if dt.configuration.running_os == "windows" then + sanitized_str = _sanitize_windows(str) + else + sanitized_str = _sanitize_posix(str) + end else - return _sanitize_posix(str) + sanitized_str = str end + log.log_level(old_log_level) + return sanitized_str end dtutils_string.libdoc.functions["sanitize_lua"] = { @@ -288,9 +358,16 @@ dtutils_string.libdoc.functions["sanitize_lua"] = { } function dtutils_string.sanitize_lua(str) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + str = string.gsub(str, "%%", "%%%%") str = string.gsub(str, "%-", "%%-") str = string.gsub(str, "%(", "%%(") str = string.gsub(str, "%)", "%%)") + str = string.gsub(str, "%[", "%%[") + str = string.gsub(str, "%]", "%%]") + str = string.gsub(str, "+", "%%+") + log.log_level(old_log_level) return str end @@ -313,7 +390,9 @@ dtutils_string.libdoc.functions["split_filepath"] = { } function dtutils_string.split_filepath(str) - -- strip out single quotes from quoted pathnames + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + -- strip out single quotes from quoted pathnames str = string.gsub(str, "'", "") str = string.gsub(str, '"', '') local result = {} @@ -323,6 +402,7 @@ function dtutils_string.split_filepath(str) result["basename"] = result["filetype"] result["filetype"] = "" end + log.log_level(old_log_level) return result end @@ -344,7 +424,10 @@ dtutils_string.libdoc.functions["get_path"] = { } function dtutils_string.get_path(str) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) local parts = dtutils_string.split_filepath(str) + log.log_level(old_log_level) return parts["path"] end @@ -366,7 +449,10 @@ dtutils_string.libdoc.functions["get_filename"] = { } function dtutils_string.get_filename(str) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) local parts = dtutils_string.split_filepath(str) + log.log_level(old_log_level) return parts["filename"] end @@ -389,7 +475,10 @@ dtutils_string.libdoc.functions["get_basename"] = { } function dtutils_string.get_basename(str) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) local parts = dtutils_string.split_filepath(str) + log.log_level(old_log_level) return parts["basename"] end @@ -411,10 +500,732 @@ dtutils_string.libdoc.functions["get_filetype"] = { } function dtutils_string.get_filetype(str) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) local parts = dtutils_string.split_filepath(str) + log.log_level(old_log_level) return parts["filetype"] end +dtutils_string.libdoc.functions["build_substitute_list"] = { + Name = [[build_substitute_list]], + Synopsis = [[build a list of variable substitutions]], + Usage = [[local ds = require "lib/dtutils.string" + ds.build_substitute_list(image, sequence, variable_string, [username], [pic_folder], [home], [desktop]) + image - dt_lua_image_t - the image being processed + sequence - integer - the sequence number of the image being processed (exported) + variable_string - string - the substitution variable string + [username] - string - optional - user name. Will be determined if not supplied + [pic_folder] - string - optional - pictures folder name. Will be determined if not supplied + [home] - string - optional - home directory. Will be determined if not supplied + [desktop] - string - optional - desktop directory. Will be determined if not supplied]], + Description = [[build_substitute_list populates variables with values from the arguments + and determined from the system and darktable.]], + Return_Value = [[]], + Limitations = [[If the value for a variable can not be determined, or if it is not supported, + then an empty string is used for the value.]], + Example = [[]], + See_Also = [[https://docs.darktable.org/usermanual/4.6/en/special-topics/variables/]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +local substitutes = {} +local category_substitutes = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local PLACEHOLDERS = {"ROLL.NAME", + "FILE.FOLDER", + "FILE.NAME", + "FILE.EXTENSION", + "ID", + "VERSION", + "VERSION.IF.MULTI", + "VERSION.NAME", + "DARKTABLE.VERSION", + "DARKTABLE.NAME", -- Not Implemented + "SEQUENCE", + "WIDTH.SENSOR", + "HEIGHT.SENSOR", + "WIDTH.RAW", + "HEIGHT.RAW", + "WIDTH.CROP", + "HEIGHT.CROP", + "WIDTH.EXPORT", + "HEIGHT.EXPORT", + "WIDTH.MAX", -- Not Implemented + "HEIGHT.MAX", -- Not Implemented + "YEAR", + "YEAR.SHORT", + "MONTH", + "MONTH.LONG", + "MONTH.SHORT", + "DAY", + "HOUR", + "HOUR.AMPM", -- Not Implemented + "MINUTE", + "SECOND", + "MSEC", + "EXIF.YEAR", + "EXIF.YEAR.SHORT", + "EXIF.MONTH", + "EXIF.MONTH.LONG", + "EXIF.MONTH.SHORT", + "EXIF.DAY", + "EXIF.HOUR", + "EXIF.HOUR.AMPM", -- Not Implemented + "EXIF.MINUTE", + "EXIF.SECOND", + "EXIF.MSEC", + "EXIF.DATE.REGIONAL", -- Not Implemented + "EXIF.TIME.REGIONAL", -- Not Implemented + "EXIF.ISO", + "EXIF.EXPOSURE", + "EXIF.EXPOSURE.BIAS", + "EXIF.EXPOSURE.PROGRAM", -- Not Implemented + "EXIF.APERTURE", + "EXIF.CROP.FACTOR", + "EXIF.FOCAL.LENGTH", + "EXIF.FOCAL.LENGTH.EQUIV", -- Not Implemented + "EXIF.FOCUS.DISTANCE", + "EXIF.MAKER", + "EXIF.MODEL", + "EXIF.WHTIEBALANCE", -- Not Implemented + "EXIF.METERING", -- Not Implemented + "EXIF.LENS", + "EXIF.FLASH.ICON", -- Not Implemented + "EXIF.FLASH", -- Not Implemented + "GPS.LONGITUDE", -- Not Implemented + "GPS.LATITUDE", -- Not Implemented + "GPS.ELEVATION", -- Not Implemented + "GPS.LOCATION.ICON", -- Not Implemented + "LONGITUDE", + "LATITUDE", + "ELEVATION", + "GPS.LOCATION", -- Not Implemented + "STARS", + "RATING.ICONS", -- Not Implemented + "LABELS", + "LABELS.ICONS", -- Not Implemented + "TITLE", + "DESCRIPTION", + "CREATOR", + "PUBLISHER", + "RIGHTS", + "TAGS", -- Not Implemented + "SIDECAR.TXT", -- Not Implemented + "FOLDER.PICTURES", + "FOLDER.HOME", + "FOLDER.DESKTOP", + "OPENCL.ACTIVATED", -- Not Implemented + "USERNAME", + "NL", -- Not Implemented + "JOBCODE" -- Not Implemented +} + +local PS = dt.configuration.running_os == "windows" and "\\" or "/" +local USER = os.getenv("USERNAME") +local HOME = dt.configuration.running_os == "windows" and os.getenv("HOMEPATH") or os.getenv("HOME") +local PICTURES = HOME .. PS .. (dt.configuration.running_os == "windows" and "My Pictures" or "Pictures") +local DESKTOP = HOME .. PS .. "Desktop" + +local function get_colorlabels(image) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + + local colorlabels = {} + + if image.red then table.insert(colorlabels, "red") end + if image.yellow then table.insert(colorlabels, "yellow") end + if image.green then table.insert(colorlabels, "green") end + if image.blue then table.insert(colorlabels, "blue") end + if image.purple then table.insert(colorlabels, "purple") end + + local labels = #colorlabels == 1 and colorlabels[1] or du.join(colorlabels, "_") + + log.log_level(old_log_level) + + return labels +end + +-- find the $CATEGORYn and $CATEGORY[n,m] requests and add them to the substitute list + +local function build_category_substitution_list(image, variable_string) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + + for match in string.gmatch(variable_string, "%$%(.-%)?%)") do -- grab each complete variable + log.msg(log.info, "match is " .. match) + + local var = string.match(match, "%$%((.-%)?)%)") -- strip of the leading $( and trailing ) + log.msg(log.info, "var is " .. var) + + if string.match(var, "CATEGORY%d") or string.match(var, "CATEGORY%[") then + local element + local tag + + if string.match(var, "CATEGORY%d") then + element, tag = string.match(var, "CATEGORY(%d)%((.-)%)") -- get the element number and the tag to match + else + element, tag = string.match(var, "%[(%d),(.-)%]") -- new syntax + end + + element = element + 1 -- add one to element since lua arrays are 1 based + log.msg(log.debug, "element is " .. element .. " and tag is " .. tag) + + local tags = image:get_tags() + log.msg(log.debug, "got " .. #tags .. " from image " .. image.filename) + + for _, image_tag in ipairs(tags) do + log.msg(log.debug, "checking tag " .. image_tag.name) + + if string.match(image_tag.name, tag) then + fields = du.split(image_tag.name, "|") + + if element <= #fields then + substitutes[var] = fields[element] + else + substitutes[var] = "" + log.msg(log.warn, "requested field for tag " .. tag .. " doesn't exist") + end + + log.msg(log.info, "set substitute for " .. var .. " to " .. fields[element]) + + end + end + end + end + log.log_level(old_log_level) +end + +-- convert image.exif_datetime_taken to system time + +local function exiftime2systime(exiftime) + local yr,mo,dy,h,m,s = string.match(exiftime, "(%d-):(%d-):(%d-) (%d-):(%d-):(%d+)") + return(os.time{year=yr, month=mo, day=dy, hour=h, min=m, sec=s}) +end + +-- build the argument substitution list from each image + +function dtutils_string.build_substitute_list(image, sequence, variable_string, username, pic_folder, home, desktop) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + + -- is time millisecond aware? Implemented in API 9.1.0 + + local is_api_9_1 = true + if dt.configuration.api_version_string < "9.1.0" then + is_api_9_1 = false + end + + local is_api_9_4 = dt.configuration.api_version_string >= "9.4.0" and true or false + + local datetime = os.date("*t") + local long_month = os.date("%B") + local short_month = os.date("%b") + local user_name = username or USER + local pictures_folder = pic_folder or PICTURES + local home_folder = home or HOME + local desktop_folder = desktop or DESKTOP + + local labels = get_colorlabels(image) + + local eyear, emon, eday, ehour, emin, esec, emsec + if dt.preferences.read("darktable", "lighttable/ui/milliseconds", "bool") and is_api_9_1 then + eyear, emon, eday, ehour, emin, esec, emsec = + string.match(image.exif_datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)%.(%d+)$") + else + emsec = "0" + eyear, emon, eday, ehour, emin, esec = + string.match(image.exif_datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)$") + end + + 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 + dtutils_string.get_basename(image.filename),-- FILE.NAME + dtutils_string.get_filetype(image.filename),-- FILE.EXTENSION + image.id, -- ID + image.duplicate_index, -- VERSION + version_multi, -- VERSION.IF_MULTI + image.version_name, -- VERSION.NAME + dt.configuration.version, -- DARKTABLE.VERSION + "", -- DARKTABLE.NAME + string.format("%04d", sequence), -- SEQUENCE + image.width, -- WIDTH.SENSOR + image.height, -- HEIGHT.SENSOR + is_api_9_1 and image.p_width or "", -- WIDTH.RAW + is_api_9_1 and image.p_height or "", -- HEIGHT.RAW + is_api_9_1 and image.final_width or "", -- WIDTH.CROP + is_api_9_1 and image.final_height or "", -- HEIGHT.CROP + is_api_9_1 and image.final_width or "", -- WIDTH.EXPORT + is_api_9_1 and image.final_height or "", -- HEIGHT.EXPORT + "", -- WIDTH.MAX -- from export module + "", -- HEIGHT.MAX -- from export module + string.format("%4d", datetime.year), -- YEAR + string.sub(datetime.year, 3), -- YEAR.SHORT + string.format("%02d", datetime.month), -- MONTH + long_month, -- MONTH.LONG + short_month, -- MONTH.SHORT + string.format("%02d", datetime.day), -- DAY + string.format("%02d", datetime.hour), -- HOUR + "", -- HOUR.AMPM + string.format("%02d", datetime.min), -- MINUTE + string.format("%02d", datetime.sec), -- SECOND + 0, -- MSEC + eyear, -- EXIF.YEAR + string.sub(eyear, 3), -- EXIF.YEAR.SHORT + emon, -- EXIF.MONTH + os.date("%B", exiftime2systime(image.exif_datetime_taken)), -- EXIF.MONTH.LONG + os.date("%b", exiftime2systime(image.exif_datetime_taken)), -- EXIF.MONTH.SHORT + eday, -- EXIF.DAY + ehour, -- EXIF.HOUR + "", -- EXIF.HOUR.AMPM + emin, -- EXIF.MINUTE + esec, -- EXIF.SECOND + emsec, -- EXIF.MSEC + "", -- EXIF.DATE.REGIONAL - wont be implemented + "", -- EXIF.TIME.REGIONAL - wont be implemented + string.format("%d", image.exif_iso), -- EXIF.ISO + string.format("%.0f", 1./image.exif_exposure), -- EXIF.EXPOSURE + image.exif_exposure_bias, -- EXIF.EXPOSURE.BIAS + "", -- EXIF.EXPOSURE.PROGRAM + string.format("%.01f", image.exif_aperture), -- EXIF.APERTURE + string.format("%.01f", image.exif_crop),-- EXIF.CROP_FACTOR + string.format("%.0f", image.exif_focal_length), -- EXIF.FOCAL.LENGTH + string.format("%.0f", image.exif_focal_length * image.exif_crop), -- EXIF.FOCAL.LENGTH.EQUIV + image.exif_focus_distance, -- EXIF.FOCUS.DISTANCE + image.exif_maker, -- EXIF.MAKER + image.exif_model, -- EXIF.MODEL + is_api_9_4 and image.exif_whitebalance or "", -- EXIF.WHITEBALANCE + is_api_9_4 and image.exif_metering_mode or "", -- EXIF.METERING + image.exif_lens, -- LENS + "", -- EXIF.FLASH.ICON + is_api_9_4 and image.exif_flash or "", -- EXIF.FLASH + "", -- GPS.LONGITUDE + "", -- GPS.LATITUDE + "", -- GPS.ELEVATION + "", -- GPS.LOCATION.ICON + image.longitude or "", -- LONGITUDE + image.latitude or "", -- LATITUDE + image.elevation or "", -- ELEVATION + "", -- GPS.LOCATION - wont be implemented + image.rating, -- STARS + "", -- RATING.ICONS - wont be implemented + labels, -- LABELS + "", -- LABELS.ICONS - wont be implemented + image.title, -- TITLE + image.description, -- DESCRIPTION + image.creator, -- CREATOR + image.publisher, -- PUBLISHER + image.rights, -- RIGHTS + "", -- TAGS - wont be implemented + "", -- SIDECAR.TXT - wont be implemented + pictures_folder, -- FOLDER.PICTURES + home_folder, -- FOLDER.HOME + desktop_folder, -- FOLDER.DESKTOP + "", -- OPENCL.ACTIVATED - wont be implemented + user_name, -- USERNAME + "", -- NL - wont be implemented + "" -- JOBCODE - wont be implemented + } + + -- populate the substitution list + + for i = 1, #PLACEHOLDERS, 1 do + substitutes[PLACEHOLDERS[i]] = replacements[i] + log.msg(log.info, "setting " .. PLACEHOLDERS[i] .. " to " .. tostring(replacements[i])) + end + + -- do category substitutions separately + + build_category_substitution_list(image, variable_string) + + log.log_level(old_log_level) +end + +dtutils_string.libdoc.functions["get_substitution_tooltip"] = { + Name = [[get_substitution_tooltip]], + Synopsis = [[get a tooltip that lists the substitution variables]], + Usage = [[local ds = require "lib/dtutils.string" + ds.get_substitution_tooltip() + Description = [[get_substitution_tooltip lists the variables with brief explanations]], + Return_Value = [[string - the tooltip]], + Limitations = [[]], + Example = [[]], + See_Also = [[https://docs.darktable.org/usermanual/4.6/en/special-topics/variables/]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +function dtutils_string.get_substitution_tooltip() + + return table.concat({ + _("$(ROLL.NAME) - roll of the input image"), + _("$(FILE.FOLDER) - folder containing the input image"), + _("$(FILE.NAME) - basename of the input image"), + _("$(FILE.EXTENSION) - extension of the input image"), + _("$(ID) - image ID"), + _("$(VERSION) - duplicate version"), + _("$(VERSION.IF_MULTI) - same as $(VERSION) but null string if only one version exists"), + _("$(VERSION.NAME) - version name from metadata"), + _("$(DARKTABLE.VERSION) - current darktable version"), + -- _("$(DARKTABLE.NAME) - darktable name"), -- not implemented + _("$(SEQUENCE[n,m]) - sequence number, n: number of digits, m: start number"), + _("$(WIDTH.SENSOR) - image sensor width"), + _("$(HEIGHT.SENSOR) - image sensor height"), + _("$(WIDTH.RAW) - RAW image width"), + _("$(HEIGHT.RAW) - RAW image height"), + _("$(WIDTH.CROP) - image width after crop"), + _("$(HEIGHT.CROP) - image height after crop"), + _("$(WIDTH.EXPORT) - exported image width"), + _("$(HEIGHT.EXPORT) - exported image height"), + -- _("$(WIDTH.MAX) - maximum image export width"), -- not implemented + -- _("$(HEIGHT.MAX) - maximum image export height"), -- not implemented + _("$(YEAR) - year"), + _("$(YEAR.SHORT) - year without century"), + _("$(MONTH) - month"), + _("$(MONTH.LONG) - full month name according to the current locale"), + _("$(MONTH.SHORT) - abbreviated month name according to the current locale"), + _("$(DAY) - day"), + _("$(HOUR) - hour"), + -- _("$(HOUR.AMPM) - hour, 12-hour clock"), -- not implemented + _("$(MINUTE) - minute"), + _("$(SECOND) - second"), + _("$(MSEC) - millisecond"), + _("$(EXIF.YEAR) - EXIF year"), + _("$(EXIF.YEAR.SHORT) - EXIF year without century"), + _("$(EXIF.MONTH) - EXIF month"), + _("$(EXIF.MONTH.LONG) - full EXIF month name according to the current locale"), + _("$(EXIF.MONTH.SHORT) - abbreviated EXIF month name according to the current locale"), + _("$(EXIF.DAY) - EXIF day"), + _("$(EXIF.HOUR) - EXIF hour"), + -- _("$(EXIF.HOUR.AMPM) - EXIF hour, 12-hour clock") .. "\n" .. -- not implemented + _("$(EXIF.MINUTE) - EXIF minute"), + _("$(EXIF.SECOND) - EXIF second"), + _("$(EXIF.MSEC) - EXIF millisecond"), + -- _("$(EXIF.DATE.REGIONAL) - localized EXIF date"), -- not implemented + -- _("$(EXIF.TIME.REGIONAL) - localized EXIF time"), -- not implemented + _("$(EXIF.ISO) - ISO value"), + _("$(EXIF.EXPOSURE) - EXIF exposure"), + _("$(EXIF.EXPOSURE.BIAS) - EXIF exposure bias"), + -- _("$(EXIF.EXPOSURE.PROGRAM) - EXIF exposure program"), -- not implemented + _("$(EXIF.APERTURE) - EXIF aperture"), + _("$(EXIF.CROP_FACTOR) - EXIF crop factor"), + _("$(EXIF.FOCAL.LENGTH) - EXIF focal length"), + _("$(EXIF.FOCAL.LENGTH.EQUIV) - EXIF 35 mm equivalent focal length"), + _("$(EXIF.FOCUS.DISTANCE) - EXIF focal distance"), + _("$(EXIF.MAKER) - camera maker") .. + _("$(EXIF.MODEL) - camera model") .. + _("$(EXIF.WHITEBALANCE) - EXIF selected white balance") .. -- not implemented + _("$(EXIF.METERING) - EXIF exposure metering mode") .. -- not implemented + _("$(EXIF.LENS) - lens") .. + -- _("$(EXIF.FLASH.ICON) - icon indicating whether flash was used") .. -- not implemented + _("$(EXIF.FLASH) - was flash used (yes/no/--)") .. -- not implemented + -- _("$(GPS.LONGITUDE) - longitude"),-- not implemented + -- _("$(GPS.LATITUDE) - latitude"),-- not implemented + -- _("$(GPS.ELEVATION) - elevation"),-- not implemented + -- _("$(GPS.LOCATION.ICON) - icon indicating whether GPS location is known"),-- not implemented + _("$(LONGITUDE) - longitude"), + _("$(LATITUDE) - latitude"), + _("$(ELEVATION) - elevation"), + _("$(STARS) - star rating as number (-1 for rejected)"), + -- _("$(RATING.ICONS) - star/reject rating in icon form"),-- not implemented + _("$(LABELS) - color labels as text"), + -- _("$(LABELS.ICONS) - color labels as icons"),-- not implemented + _("$(TITLE) - title from metadata"), + _("$(DESCRIPTION) - description from metadata"), + _("$(CREATOR) - creator from metadata"), + _("$(PUBLISHER) - publisher from metadata"), + _("$(RIGHTS) - rights from metadata"), + --_("$(TAGS) - tags as set in metadata settings"), + _("$(CATEGORY[n,category]) - subtag of level n in hierarchical tags"), + _("$(SIDECAR_TXT) - contents of .txt sidecar file, if present"), + _("$(FOLDER.PICTURES) - pictures folder"), + _("$(FOLDER.HOME) - home folder"), + _("$(FOLDER.DESKTOP) - desktop folder"), + -- _("$(OPENCL.ACTIVATED) - whether OpenCL is activated"), + _("$(USERNAME) - login name"), + -- _("$(NL) - newline"), + -- _("$(JOBCODE) - job code for import"), + ""}, "\n") +end + +-- handle different versions of names + +local function check_legacy_vars(var_name) + local var = var_name + + if string.match(var, "_") then + var = string.gsub(var, "_", ".") + end + + if string.match(var, "^HOME$") then var = "FOLDER.HOME" end + if string.match(var, "^PICTURES.FOLDER$") then var = "FOLDER.PICTURES" end + if string.match(var, "^DESKTOP$") then var = "FOLDER.DESKTOP" end + + return var +end + +-- get the substitution and do any string manipulations requested + +local function treat(var_string) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + + local ret_val = "" + + -- remove the var from the string + local var = string.match(var_string, "[%a%._]+") + + var = check_legacy_vars(var) + log.msg(log.info, "var_string is " .. tostring(var_string) .. " and var is " .. tostring(var)) + + if string.match(var_string, "CATEGORY%d") or string.match(var_string, "CATEGORY%[") then + log.msg(log.info, "substituting for " .. var_string) + ret_val = substitutes[var_string] + if not ret_val then ret_val = "" end + log.msg(log.info, "ret_val is " .. ret_val) + + elseif string.match(var_string, "SEQUENCE%[") then + local width, start = string.match(var_string, "(%d+),(%d)") + local seq_val = tonumber(substitutes[var]) + local pat = "%0" .. width .. "d" + substitutes[var_string] = string.format(pat, start + (seq_val - 1)) + ret_val = substitutes[var_string] + + else + ret_val = substitutes[var] + end + + local valid_var = false + + if ret_val then + valid_var = true + end + + if not valid_var then + log.msg(log.error, "variable " .. var .. " is not an allowed variable, returning empty value") + log.log_level(old_log_level) + return "" + end + + -- string modifications + + local args = string.gsub(var_string, var, "") + log.msg(log.info, "args is " .. tostring(args)) + + if string.len(args) > 0 then + + if string.match(args, '^%^%^') then + ret_val = string.upper(ret_val) + + elseif string.match(args, "^%^") then + ret_val = string.gsub(ret_val, "^%a", string.upper, 1) + + elseif string.match(args, "^,,") then + ret_val = string.lower(ret_val) + + elseif string.match(args, "^,") then + ret_val = string.gsub(ret_val, "^%a", string.lower, 1) + + elseif string.match(args, "^:%-?%d+:%-?%d+") then + + local soffset, slen = string.match(args, ":(%-?%d+):(%-?%d+)") + log.msg(log.info, "soffset is " .. soffset .. " and slen is " .. slen) + + if tonumber(soffset) >= 0 then + soffset = soffset + 1 + end + log.msg(log.info, "soffset is " .. soffset .. " and slen is " .. slen) + + if tonumber(soffset) < 0 and tonumber(slen) < 0 then + local temp = soffset + soffset = slen + slen = temp + end + log.msg(log.info, "soffset is " .. soffset .. " and slen is " .. slen) + + ret_val = string.sub(ret_val, soffset, slen) + log.msg(log.info, "ret_val is " .. ret_val) + + elseif string.match(args, "^:%-?%d+") then + + local soffset= string.match(args, ":(%-?%d+)") + if tonumber(soffset) >= 0 then + soffset = soffset + 1 + end + ret_val = string.sub(ret_val, soffset, -1) + + elseif string.match(args, "^-%$%(.-%)") then + + local replacement = string.match(args, "-%$%(([%a%._]+)%)") + replacement = check_legacy_vars(replacement) + if string.len(ret_val) == 0 then + ret_val = substitutes[replacement] + end + + elseif string.match(args, "^-.+$") then + + local replacement = string.match(args, "-(.+)$") + if string.len(ret_val) == 0 then + ret_val = replacement + end + + elseif string.match(args, "^+.+") then + + local replacement = string.match(args, "+(.+)") + if string.len(ret_val) > 0 then + ret_val = replacement + end + + elseif string.match(args, "^#.+") then + + local pattern = string.match(args, "#(.+)") + log.msg(log.info, "pattern to remove is " .. tostring(pattern)) + ret_val = string.gsub(ret_val, "^" .. dtutils_string.sanitize_lua(pattern), "") + + elseif string.match(args, "^%%.+") then + + local pattern = string.match(args, "%%(.+)") + ret_val = string.gsub(ret_val, pattern .. "$", "") + + elseif string.match(args, "^//.-/.+") then + + local pattern, replacement = string.match(args, "//(.-)/(.+)") + ret_val = string.gsub(ret_val, pattern, replacement) + + elseif string.match(args, "^/#.+/.+") then + + local pattern, replacement = string.match(args, "/#(.+)/(.+)") + ret_val = string.gsub(ret_val, "^" .. pattern, replacement, 1) + + elseif string.match(args, "^/%%.-/.+") then + + local pattern, replacement = string.match(args, "/%%(.-)/(.+)") + ret_val = string.gsub(ret_val, pattern .. "$", replacement) + + elseif string.match(args, "^/.-/.+") then + + log.msg(log.info, "took replacement branch") + local pattern, replacement = string.match(args, "/(.-)/(.+)") + ret_val = string.gsub(ret_val, pattern, replacement, 1) + end + + end + log.log_level(old_log_level) + return ret_val +end + +dtutils_string.libdoc.functions["substitute_list"] = { + Name = [[substitute_list]], + Synopsis = [[Replace variables in a string with their computed values]], + Usage = [[local ds = require "lib/dtutils.string" + local result = ds.substitute_list(str) + str - string - the string containing the variables to be substituted for]], + Description = [[substitute_list replaces the variables in the supplied string with + values computed in build_substitution_list().]], + Return_Value = [[result - string - the input string with values substituted for the variables]], + Limitations = [[]], + Example = [[]], + See_Also = [[https://docs.darktable.org/usermanual/4.6/en/special-topics/variables/]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +function dtutils_string.substitute_list(str) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + + -- replace the substitution variables in a string + for match in string.gmatch(str, "%$%(.-%)?%)") do + + local var = string.match(match, "%$%((.-%)?)%)") + + local treated_var = treat(var) + log.msg(log.info, "var is " .. var .. " and treated var is " .. tostring(treated_var)) + + str = string.gsub(str, "%$%(".. dtutils_string.sanitize_lua(var) .."%)", tostring(treated_var)) + log.msg(log.info, "str after replacement is " .. str) + + end + + log.log_level(old_log_level) + + return str +end + +dtutils_string.libdoc.functions["clear_substitute_list"] = { + Name = [[clear_substitute_list]], + Synopsis = [[Clear the computed list of variable substitution values]], + Usage = [[local ds = require "lib/dtutils.string" + ds.clear_substitute_list()]], + Description = [[clear_substitute_list resets the list of variable replacement values]], + Return_Value = [[]], + Limitations = [[]], + Example = [[]], + See_Also = [[]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +function dtutils_string.clear_substitute_list() + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + + substitutes = {} + category_substitutes = {} + + log.log_level(old_log_level) +end + +dtutils_string.libdoc.functions["substitute"] = { + Name = [[substitute]], + Synopsis = [[Perform all the variable substitution steps with one function call]], + Usage = [[local ds = require "lib/dtutils.string" + ds.substitute(image, sequence, variable_string, [username], [pic_folder], [home], [desktop]) + image - dt_lua_image_t - the image being processed + sequence - integer - the number of the image being processed (exported) + variable_string - string - the substitution variable string + [username] - string - optional - user name. Will be determined if not supplied + [pic_folder] - string - optional - pictures folder name. Will be determined if not supplied + [home] - string - optional - home directory. Will be determined if not supplied + [desktop] - string - optional - desktop directory. Will be determined if not supplied]], + Description = [[substitute initializes the substitution list by calling clear_substitute_list(), + then builds the substitutions by calling build_substitute_list() and finally does the + substitution by calling substitute_list(), then returns the result string.]], + Return_Value = [[result - string - the input string with values substituted for the variables]], + Limitations = [[]], + Example = [[]], + See_Also = [[https://docs.darktable.org/usermanual/4.6/en/special-topics/variables/]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +function dtutils_string.substitute(image, sequence, variable_string, username, pic_folder, home, desktop) + local old_log_level = log.log_level() + log.log_level(dtutils_string.log_level) + + dtutils_string.clear_substitute_list() + + dtutils_string.build_substitute_list(image, sequence, variable_string, username, pic_folder, home, desktop) + + local str = dtutils_string.substitute_list(variable_string) + + log.log_level(old_log_level) + + return str +end + return dtutils_string diff --git a/lib/dtutils/system.lua b/lib/dtutils/system.lua index f41f821c..10ea29f3 100644 --- a/lib/dtutils/system.lua +++ b/lib/dtutils/system.lua @@ -1,6 +1,7 @@ local dtutils_system = {} local dt = require "darktable" +local ds = require "lib/dtutils.string" dtutils_system.libdoc = { Name = [[dtutils.system]], @@ -85,8 +86,13 @@ function dtutils_system.windows_command(command) local file = io.open(fname, "w") if file then dt.print_log("opened file") - command = string.gsub(command, "%%", "%%%%") -- escape % from windows shell - file:write(command) + file:write("@echo off\n") + file:write('for /f "tokens=2 delims=:." %%x in (\'chcp\') do set cp=%%x\n') + file:write("chcp 65001>nul\n") -- change the encoding of the terminal to handle non-english characters in path + file:write("\n") + file:write(command .. "\n") + file:write("\n") + file:write("chcp %cp%>nul\n") file:close() result = dt.control.execute(fname) @@ -118,6 +124,7 @@ dtutils_system.libdoc.functions["launch_default_app"] = { License = [[]], Copyright = [[]], } + function dtutils_system.launch_default_app(path) local open_cmd = "xdg-open " if (dt.configuration.running_os == "windows") then @@ -128,5 +135,4 @@ function dtutils_system.launch_default_app(path) return dtutils_system.external_command(open_cmd .. path) end - return dtutils_system diff --git a/locale/de_DE/LC_MESSAGES/clear_GPS.po b/locale/de_DE/LC_MESSAGES/clear_GPS.po index ad8960b0..420ec484 100644 --- a/locale/de_DE/LC_MESSAGES/clear_GPS.po +++ b/locale/de_DE/LC_MESSAGES/clear_GPS.po @@ -14,7 +14,7 @@ msgstr "" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=CHARSET\n" +"Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.5.4\n" "Language: de_DE\n" diff --git a/official/apply_camera_style.lua b/official/apply_camera_style.lua new file mode 100644 index 00000000..fe836c3b --- /dev/null +++ b/official/apply_camera_style.lua @@ -0,0 +1,498 @@ +--[[ + + apply_camera_style.lua - apply camera style to matching images + + Copyright (C) 2024 Bill Ferguson . + + This program 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. + + This program 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 this program. If not, see . +]] +--[[ + apply_camera_style - apply darktable camera style to matching images + + apply a camera style corresponding to the camera used to + take the image to provide a starting point for editing that + is similar to the SOOC jpeg. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + none + + USAGE + start the script from script_manager + + BUGS, COMMENTS, SUGGESTIONS + Bill Ferguson + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +-- local df = require "lib/dtutils.file" +-- local ds = require "lib/dtutils.string" +-- local dtsys = require "lib/dtutils.system" +local log = require "lib/dtutils.log" +-- local debug = require "darktable.debug" + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "apply_camera_style" +local DEFAULT_LOG_LEVEL = log.info +local TMP_DIR = dt.configuration.tmp_dir +local STYLE_PREFIX = "_l10n_darktable|_l10n_camera styles|" +local MAKER = 3 +local STYLE = 4 + +-- path separator +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +-- command separator +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) -- camera styles added to darktable 5.0 + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- I 1 8 N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- S C R I P T M A N A G E R I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local script_data = {} + +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 +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 + +script_data.metadata = { + name = _("apply camera style"), -- name of script + purpose = _("apply darktable camera style to matching images"), -- purpose of script + author = "Bill Ferguson ", -- your name and optionally e-mail address + help = "https://docs.darktable.org/lua/development/lua.scripts.manual/scripts/official/apply_camera_style/" -- URL to help/documentation +} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- L O G L E V E L +-- - - - - - - - - - - - - - - - - - - - - - - - + +log.log_level(DEFAULT_LOG_LEVEL) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local apply_camera_style = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- G L O B A L V A R I A B L E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +apply_camera_style.imported_images = {} +apply_camera_style.styles = {} +apply_camera_style.log_level = DEFAULT_LOG_LEVEL + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- P R E F E R E N C E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A L I A S E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local namespace = apply_camera_style +local acs = apply_camera_style + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +------------------- +-- helper functions +------------------- + +local function set_log_level(level) + local old_log_level = log.log_level() + log.log_level(level) + return old_log_level +end + +local function restore_log_level(level) + log.log_level(level) +end + +------------------- +-- script functions +------------------- + +local function process_pattern(pattern) + + local log_level = set_log_level(acs.log_level) + + pattern = string.lower(pattern) + -- strip off series + pattern = string.gsub(pattern, " series$", "?") + -- match a character + if string.match(pattern, "?$") then + -- handle EOS R case + pattern = string.gsub(pattern, "?", ".?") + else + pattern = string.gsub(pattern, "?", ".") + end + pattern = string.gsub(pattern, " ", " ?") + -- escape dashes + pattern = string.gsub(pattern, "%-", "%%-") + -- make spaces optional + pattern = string.gsub(pattern, " ", " ?") + -- until we end up with a set, I'll defer set processing, i.e. [...] + -- anchor the pattern to ensure we don't short match + pattern = "^" .. pattern .. "$" + + restore_log_level(log_level) + + return pattern +end + +local function process_set(pattern_set) + + local log_level = set_log_level(acs.log_level) + + local to_process = {} + local processed = {} + + local base, set, tail + + if string.match(pattern_set, "]$") then + base, set = string.match(pattern_set, "(.+)%[(.+)%]") + else + base, set, tail = string.match(pattern_set, "(.+)%[(.+)%](.+)") + end + + log.msg(log.debug, "base is " .. base .. " and set is " .. set) + + to_process = du.split(set, ",") + + for _, item in ipairs(to_process) do + local pat = base .. item + if tail then + pat = pat .. tail + end + table.insert(processed, process_pattern(pat)) + end + + restore_log_level(log_level) + + return processed +end + +local function get_camera_styles() + + local log_level = set_log_level(acs.log_level) + + -- separate the styles into + -- + -- acs.styles - + -- maker - + -- styles {} + -- patterns {} + + for _, style in ipairs(dt.styles) do + + if string.match(style.name, STYLE_PREFIX) then + log.msg(log.debug, "got " .. style.name) + + local parts = du.split(style.name, "|") + parts[MAKER] = string.lower(parts[MAKER]) + log.msg(log.debug, "maker is " .. parts[MAKER]) + + if not acs.styles[parts[MAKER]] then + acs.styles[parts[MAKER]] = {} + acs.styles[parts[MAKER]]["styles"] = {} + acs.styles[parts[MAKER]]["patterns"] = {} + end + if parts[STYLE] then + if not string.match(parts[STYLE], "]") then + table.insert(acs.styles[parts[MAKER]].styles, style) + local processed_pattern = process_pattern(parts[#parts]) + table.insert(acs.styles[parts[MAKER]].patterns, processed_pattern) + log.msg(log.debug, "pattern for " .. style.name .. " is " .. processed_pattern) + else + local processed_patterns = process_set(parts[STYLE]) + for _, pat in ipairs(processed_patterns) do + table.insert(acs.styles[parts[MAKER]].styles, style) + table.insert(acs.styles[parts[MAKER]].patterns, pat) + log.msg(log.debug, "pattern for " .. style.name .. " is " .. pat) + end + end + end + end + end + + restore_log_level(log_level) + +end + +local function normalize_model(maker, model) + + local log_level = set_log_level(acs.log_level) + + model = string.lower(model) + + -- strip off the maker name + if maker == "canon" then + model = string.gsub(model, "canon ", "") + elseif maker == "hasselblad" then + model = string.gsub(model, "hasselblad ", "") + elseif maker == "leica" then + model = string.gsub(model, "leica ", "") + elseif maker == "lg" then + model = string.gsub(model, "lg ", "") + elseif maker == "nikon" then + model = string.gsub(model, "nikon ", "") + elseif maker == "nokia" then + model = string.gsub(model, "nokia ", "") + elseif maker == "oneplus" then + model = string.gsub(model, "oneplus ", "") + elseif maker == "pentax" then + model = string.gsub(model, "pentax ", "") + model = string.gsub(model, "ricoh ", "") + end + + restore_log_level(log_level) + + return model +end + +local function normalize_maker(maker) + + local log_level = set_log_level(acs.log_level) + + maker = string.lower(maker) + + if string.match(maker, "^fujifilm") then + maker = "fujifilm" + elseif string.match(maker, "^hmd ") then + maker = "nokia" + elseif string.match(maker, "^leica") then + maker = "leica" + elseif string.match(maker, "^minolta") then + maker = "minolta" + elseif string.match(maker, "^nikon") then + maker = "nikon" + elseif string.match(maker, "^om ") then + maker = "om system" + elseif string.match(maker, "^olympus") then + maker = "olympus" + elseif string.match(maker, "^pentax") or string.match(maker, "^ricoh") then + maker = "pentax" + end + + restore_log_level(log_level) + + return maker +end + +local function has_style_tag(image, tag_name) + + local log_level = set_log_level(acs.log_level) + + local result = false + + log.msg(log.debug, "looking for tag " .. tag_name) + + for _, tag in ipairs(image:get_tags()) do + log.msg(log.debug, "checking against " .. tag.name) + if tag.name == tag_name then + log.msg(log.debug, "matched tag " .. tag_name) + result = true + end + end + + restore_log_level(log_level) + + return result +end + +local function mangle_model(model) + + local log_level = set_log_level(acs.log_level) + + if string.match(model, "eos") then + log.msg(log.debug, "mangle model got " .. model) + model = string.gsub(model, "r6m2", "r6 mark ii") + model = string.gsub(model, "eos 350d digital", "eos kiss digital n") + model = string.gsub(model, "eos 500d", "eos rebel t1") + model = string.gsub(model, "eos 550d", "eos rebel t2") + model = string.gsub(model, "eos 600d", "eos rebel t3i") + model = string.gsub(model, "eos 650d", "eos rebel t4i") + model = string.gsub(model, "eos 700d", "eos rebel t5") + model = string.gsub(model, "eos 750d", "eos rebel t6i") + model = string.gsub(model, "eos 760d", "eos rebel t6s") + model = string.gsub(model, "eos 100d", "eos rebel t6") + model = string.gsub(model, "eos 1100d", "eos rebel t3") + model = string.gsub(model, "eos 1200d", "eos rebel t5") + model = string.gsub(model, "eos 1300d", "eos rebel t6") + model = string.gsub(model, "eos 2000d", "eos rebel t7") + log.msg(log.debug, "mandle model returning " .. model) + end + + restore_log_level(log_level) + + return model +end + +local function stop_job() + if acs.job then + if acs.job.valid then + acs.job.valid = false + end + end +end + +local function apply_style_to_images(images) + + local log_level = set_log_level(acs.log_level) + + acs.job = dt.gui.create_job(_("applying camera styles to images"), true, stop_job) + + for count, image in ipairs(images) do + local maker = normalize_maker(image.exif_maker) + local model = normalize_model(maker, image.exif_model) + model = mangle_model(model) + log.msg(log.debug, "got maker " .. maker .. " and model " .. model .. " from image " .. image.filename) + + if acs.styles[maker] then + local no_match = true + for i, pattern in ipairs(acs.styles[maker].patterns) do + if string.match(model, pattern) or + (i == #acs.styles[maker].patterns and string.match(pattern, "generic")) then + local tag_name = "darktable|style|" .. acs.styles[maker].styles[i].name + if not has_style_tag(image, tag_name) then + image:apply_style(acs.styles[maker].styles[i]) + no_match = false + log.msg(log.info, "applied style " .. acs.styles[maker].styles[i].name .. " to " .. image.filename) + end + log.log_level(loglevel) + break + end + end + if no_match then + log.msg(log.info, "no style found for " .. maker .. " " .. model) + end + else + log.msg(log.info, "no maker found for " .. image.filename) + end + if count % 10 == 0 then + acs.job.percent = count / #images + end + if dt.control.ending then + stop_job() + end + end + + stop_job() + + restore_log_level(log_level) + +end + +local function apply_camera_style(collection) + + local log_level = set_log_level(acs.log_level) + + local images = nil + + if collection == true then + images = dt.collection + log.msg(log.info, "applying camera styles to collection") + elseif collection == false then + images = dt.gui.selection() + if #images == 0 then + images = dt.gui.action_images + end + log.msg(log.info, "applying camera styles to selection") + end + apply_style_to_images(images) + + restore_log_level(log_level) + +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N P R O G R A M +-- - - - - - - - - - - - - - - - - - - - - - - - + +get_camera_styles() + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- U S E R I N T E R F A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + dt.destroy_event(MODULE, "post-import-image") + dt.destroy_event(MODULE, "post-import-film") +end + +script_data.destroy = destroy + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.register_event(MODULE, "shortcut", + function(event, shortcut) + apply_camera_style(true) + end, _("apply darktable camera styles to collection") +) + +dt.register_event(MODULE, "shortcut", + function(event, shortcut) + apply_camera_style(false) + end, _("apply darktable camera styles to selection") +) + +dt.register_event(MODULE, "post-import-image", + function(event, image) + if image.is_raw then + table.insert(acs.imported_images, image) + end + end +) + +dt.register_event(MODULE, "post-import-film", + function(event, film) + apply_style_to_images(acs.imported_images) + acs.imported_images = {} + end +) + +return script_data diff --git a/official/check_for_updates.lua b/official/check_for_updates.lua index de92c7c9..aeb45edb 100644 --- a/official/check_for_updates.lua +++ b/official/check_for_updates.lua @@ -36,8 +36,6 @@ du.check_min_api_version("2.0.0", "check_for_updates") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("check_for_updates", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -47,7 +45,7 @@ end local script_data = {} script_data.metadata = { - name = "check_for_updates", + name = _("check for updates"), purpose = _("check for newer darktable releases"), author = "Tobias Ellinghaus", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/check_for_updates" diff --git a/official/copy_paste_metadata.lua b/official/copy_paste_metadata.lua index 0c765ec1..00040c9b 100644 --- a/official/copy_paste_metadata.lua +++ b/official/copy_paste_metadata.lua @@ -31,8 +31,6 @@ local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "copy_paste_metadata") -dt.gettext.bindtextdomain("copy_paste_metadata", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -42,7 +40,7 @@ end local script_data = {} script_data.metadata = { - name = "copy_paste_metadata", + name = _("copy paste metadata"), purpose = _("adds keyboard shortcuts and buttons to copy/paste metadata between images"), author = "Tobias Ellinghaus", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/copy_paste_metadata" diff --git a/official/delete_long_tags.lua b/official/delete_long_tags.lua index 2c807e15..c770402b 100644 --- a/official/delete_long_tags.lua +++ b/official/delete_long_tags.lua @@ -35,8 +35,6 @@ du.check_min_api_version("2.0.0", "delete_long_tags") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("delete_long_tags", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -46,7 +44,7 @@ end local script_data = {} script_data.metadata = { - name = "delete_long_tags", + name = _("delete long tags"), purpose = _("delete all tags longer than a set length"), author = "Tobias Ellinghaus", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/delete_long_tags" diff --git a/official/delete_unused_tags.lua b/official/delete_unused_tags.lua index bcdceaf1..3815aea0 100644 --- a/official/delete_unused_tags.lua +++ b/official/delete_unused_tags.lua @@ -39,8 +39,6 @@ end local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("delete_unused_tags", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -50,7 +48,7 @@ end local script_data = {} script_data.metadata = { - name = "delete_unused_tags", + name = _("delete unused tags"), purpose = _("delete unused tags"), author = "Tobias Ellinghaus", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/delete_unused_tags" diff --git a/official/enfuse.lua b/official/enfuse.lua index c8df51b1..7bda2cf4 100644 --- a/official/enfuse.lua +++ b/official/enfuse.lua @@ -43,8 +43,6 @@ local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "enfuse") -dt.gettext.bindtextdomain("enfuse", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -54,7 +52,7 @@ end local script_data = {} script_data.metadata = { - name = "enfuse", + name = _("enfuse"), purpose = _("exposure blend images"), author = "Tobias Ellinghaus", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/enfuse" @@ -74,7 +72,7 @@ local function install_module() if not enf.module_installed then dt.register_lib( "enfuse", -- plugin name - "enfuse", -- name + _("enfuse"), -- name true, -- expandable false, -- resetable {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers @@ -137,7 +135,7 @@ if enfuse_installed then if version < "4.2" then exposure_mu = dt.new_widget("slider") { - label = "exposure mu", + label = _("exposure mu"), tooltip = _("center also known as mean of gaussian weighting function (0 <= mean <= 1); default: 0.5"), hard_min = 0, hard_max = 1, @@ -146,7 +144,7 @@ if enfuse_installed then else exposure_mu = dt.new_widget("slider") { - label = "exposure optimum", + label = _("exposure optimum"), tooltip = _("optimum exposure value, usually the maximum of the weighting function (0 <= optimum <=1); default 0.5"), hard_min = 0, hard_max = 1, @@ -156,7 +154,7 @@ if enfuse_installed then local depth = dt.new_widget("combobox") { - label = "depth", + label = _("depth"), tooltip = _("the number of bits per channel of the output image"), value = dt.preferences.read("enfuse", "depth", "integer"), changed_callback = function(w) dt.preferences.write("enfuse", "depth", "integer", w.selected) end, @@ -165,7 +163,7 @@ if enfuse_installed then local blend_colorspace = dt.new_widget("combobox") { - label = "blend colorspace", + label = _("blend colorspace"), tooltip = _("force blending in selected colorspace"), changed_callback = function(w) dt.preferences.write("enfuse", "blend_colorspace", "string", w.selected) end, "", "identity", "ciecam" diff --git a/official/generate_image_txt.lua b/official/generate_image_txt.lua index deacefaf..c786aabf 100644 --- a/official/generate_image_txt.lua +++ b/official/generate_image_txt.lua @@ -40,8 +40,6 @@ require "darktable.debug" local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("generate_image_txt", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -53,7 +51,7 @@ du.check_min_api_version("7.0.0", "generate_image_txt") local script_data = {} script_data.metadata = { - name = "generate_image_txt", + name = _("generate image text"), purpose = _("overlay metadata on the selected image(s)"), author = "Tobias Ellinghaus", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/generate_image_txt" diff --git a/official/image_path_in_ui.lua b/official/image_path_in_ui.lua index fd9e3fbc..2f23184a 100644 --- a/official/image_path_in_ui.lua +++ b/official/image_path_in_ui.lua @@ -35,8 +35,6 @@ du.check_min_api_version("7.0.0", "image_path_in_ui") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("image_path_in_ui", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end @@ -46,7 +44,7 @@ end local script_data = {} script_data.metadata = { - name = "image_path_in_ui", + name = _("image path in UI"), purpose = _("print the image path in the UI"), author = "Jérémy Rosen", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/image_path_in_ui" @@ -65,7 +63,7 @@ local main_label = dt.new_widget("label"){selectable = true, ellipsize = "middle local function install_module() if not ipiu.module_installed then - dt.register_lib("image_path_no_ui","selected images path",true,false,{ + dt.register_lib("image_path_no_ui",_("selected images path"),true,false,{ [dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER",300} }, main_label ) diff --git a/official/import_filter_manager.lua b/official/import_filter_manager.lua index 9b16ee3e..adf83620 100644 --- a/official/import_filter_manager.lua +++ b/official/import_filter_manager.lua @@ -34,8 +34,6 @@ local dt = require "darktable" local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("import_filter_manager", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -43,7 +41,7 @@ end local script_data = {} script_data.metadata = { - name = "import_filter_manager", + name = _("import filter manager"), purpose = _("manage import filters"), author = "Tobias Ellinghaus", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/import_filter_manager" diff --git a/official/import_filters.lua b/official/import_filters.lua index 165ec3fd..87d8b0ef 100644 --- a/official/import_filters.lua +++ b/official/import_filters.lua @@ -32,8 +32,6 @@ local dt = require "darktable" local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("import_filters", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -41,7 +39,7 @@ end local script_data = {} script_data.metadata = { - name = "import_filters", + name = _("import filters"), purpose = _("import filtering"), author = "Tobias Ellinghaus & Christian Mandel", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/import_filters" diff --git a/official/save_selection.lua b/official/save_selection.lua index 865ca265..64d33b4e 100644 --- a/official/save_selection.lua +++ b/official/save_selection.lua @@ -40,8 +40,6 @@ du.check_min_api_version("7.0.0", "save_selection") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("save_selection", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -51,7 +49,7 @@ end local script_data = {} script_data.metadata = { - name = "save_selection", + name = _("save selection"), purpose = _("shortcuts providing multiple selection buffers"), author = "Jérémy Rosen", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/save_selection" diff --git a/official/selection_to_pdf.lua b/official/selection_to_pdf.lua index 18a7be57..64b9c8be 100644 --- a/official/selection_to_pdf.lua +++ b/official/selection_to_pdf.lua @@ -40,8 +40,6 @@ du.check_min_api_version("7.0.0", "selection_to_pdf") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("selection_to_pdf", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -51,7 +49,7 @@ end local script_data = {} script_data.metadata = { - name = "selection_to_pdf", + name = _("selection to PDF"), purpose = _("generate a pdf file of selected images"), author = "Jérémy Rosen & Pascal Obry", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/selection_to_pdf" @@ -131,7 +129,7 @@ local function destroy() dt.print_log("done destroying") end -dt.register_storage(_("export_pdf"),_("export thumbnails to pdf"), +dt.register_storage("export_pdf", _("export thumbnails to pdf"), nil, function(storage,image_table) local my_title = title_widget.text diff --git a/tools/executable_manager.lua b/tools/executable_manager.lua index 58a055ab..4d03be28 100644 --- a/tools/executable_manager.lua +++ b/tools/executable_manager.lua @@ -31,13 +31,12 @@ local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" +local dtsys = require "lib/dtutils.system" du.check_min_api_version("7.0.0", "executable_manager") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("executable_manager", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -47,7 +46,7 @@ end local script_data = {} script_data.metadata = { - name = "executable_manager", + name = _("executable manager"), purpose = _("manage the list of external executables used by the lua scripts"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/tools/executable_manager" @@ -107,13 +106,19 @@ local function update_combobox_choices(combobox, choice_table, selected) end local function install_module() + local panel = "DT_UI_CONTAINER_PANEL_LEFT_BOTTOM" + local panel_pos = 600 + if dt.configuration.running_os == "windows" then + panel = "DT_UI_CONTAINER_PANEL_LEFT_CENTER" + panel_pos = 100 + end if not exec_man.module_installed then dt.register_lib( "executable_manager", -- Module name - "executable manager", -- Visible name + _("executables"), -- Visible name true, -- expandable false, -- resetable - {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_BOTTOM", 600}}, -- containers + {[dt.gui.views.lighttable] = {panel, panel_pos}}, -- containers dt.new_widget("box") -- widget { orientation = "vertical", @@ -194,7 +199,7 @@ exec_man.stack = dt.new_widget("stack"){} -- create a combobox to for indexing into the stack of widgets exec_man.selector = dt.new_widget("combobox"){ - label = "executable", + label = _("executable"), tooltip = _("select executable to modify"), value = 1, "placeholder", changed_callback = function(self) diff --git a/tools/get_lib_manpages.lua b/tools/get_lib_manpages.lua index a3c39157..0e641104 100644 --- a/tools/get_lib_manpages.lua +++ b/tools/get_lib_manpages.lua @@ -7,6 +7,7 @@ local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" +local dtsys = require "lib/dtutils.system" local log = require "lib/dtutils.log" local libname = nil @@ -14,8 +15,6 @@ du.check_min_api_version("3.0.0", "get_lib_manpages") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("get_lib_manpages", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -89,7 +88,7 @@ end local script_data = {} script_data.metadata = { - name = "get_lib_manpages", + name = _("get library man pages"), purpose = _("output the internal library documentation as man pages"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/tools/get_lib_manpages" diff --git a/tools/get_libdoc.lua b/tools/get_libdoc.lua index 5cb08122..2c4458d0 100644 --- a/tools/get_libdoc.lua +++ b/tools/get_libdoc.lua @@ -6,13 +6,12 @@ local dt = require "darktable" local du = require "lib/dtutils" +local dtsys = require "lib/dtutils.system" du.check_min_api_version("3.0.0", "get_libdoc") local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("get_libdoc", dt.configuration.config_dir .."/lua/locale/") - local function _(msg) return gettext(msg) end @@ -63,7 +62,7 @@ end local script_data = {} script_data.metadata = { - name = "get_libdoc", + name = _("get library docs"), purpose = _("retrieve and print the documentation to the console"), author = "Bill Ferguson ", help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/tools/get_libdoc" diff --git a/tools/script_manager.lua b/tools/script_manager.lua index 71609321..becb3d05 100644 --- a/tools/script_manager.lua +++ b/tools/script_manager.lua @@ -1,6 +1,6 @@ --[[ This file is part of darktable, - copyright (c) 2018, 2020, 2023 Bill Ferguson + copyright (c) 2018, 2020, 2023, 2024 Bill Ferguson darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,9 +22,9 @@ manage the lua scripts. On startup script_manager scans the lua scripts directory to see what scripts are present. - Scripts are sorted by 'category' based on what sub-directory they are in. With no - additional script repositories iinstalled, the categories are contrib, examples, official - and tools. When a category is selected the buttons show the script name and whether the + Scripts are sorted by 'folder' based on what sub-directory they are in. With no + additional script repositories iinstalled, the folders are contrib, examples, official + and tools. When a folder is selected the buttons show the script name and whether the script is started or stopped. The button is a toggle, so if the script is stopped click the button to start it and vice versa. @@ -56,42 +56,45 @@ local dtsys = require "lib/dtutils.system" local log = require "lib/dtutils.log" local debug = require "darktable.debug" --- set up translation - local gettext = dt.gettext.gettext -dt.gettext.bindtextdomain("script_manager", dt.configuration.config_dir .."/lua/locale/") - local function _(msgid) return gettext(msgid) end -- api check -du.check_min_api_version("5.0.0", "script_manager") +-- du.check_min_api_version("9.3.0", "script_manager") -- - - - - - - - - - - - - - - - - - - - - - - - -- C O N S T A N T S -- - - - - - - - - - - - - - - - - - - - - - - - +-- script_manager required API version +local SM_API_VER_REQD = "9.3.0" + -- path separator -local PS = dt.configuration.running_os == "windows" and "\\" or "/" +local PS = dt.configuration.running_os == "windows" and "\\" or "/" -- command separator -local CS = dt.configuration.running_os == "windows" and "&" or ";" +local CS = dt.configuration.running_os == "windows" and "&" or ";" + +local MODULE = "script_manager" -local MODULE = "script_manager" +local MIN_BUTTONS_PER_PAGE = 5 +local MAX_BUTTONS_PER_PAGE = 20 +local DEFAULT_BUTTONS_PER_PAGE = 10 -local MIN_BUTTONS_PER_PAGE = 5 -local MAX_BUTTONS_PER_PAGE = 20 -local DEFAULT_BUTTONS_PER_PAGE = 10 +local DEFAULT_LOG_LEVEL = log.warn -local DEFAULT_LOG_LEVEL = log.error +local LUA_DIR = dt.configuration.config_dir .. PS .. "lua" +local LUA_SCRIPT_REPO = "https://github.com/darktable-org/lua-scripts.git" -local LUA_DIR = dt.configuration.config_dir .. PS .. "lua" -local LUA_SCRIPT_REPO = "https://github.com/darktable-org/lua-scripts.git" +local LUA_API_VER = "API-" .. dt.configuration.api_version_string -local LUA_API_VER = "API-" .. dt.configuration.api_version_string +-- local POWER_ICON = dt.configuration.config_dir .. "/lua/data/data/icons/power.png" +local POWER_ICON = dt.configuration.config_dir .. "/lua/data/icons/path20.png" +local BLANK_ICON = dt.configuration.config_dir .. "/lua/data/icons/blank20.png" -- - - - - - - - - - - - - - - - - - - - - - - - -- P R E F E R E N C E S @@ -128,7 +131,8 @@ sm.event_registered = false -- set up tables to contain all the widgets and choices sm.widgets = {} -sm.categories = {} +sm.folders = {} +sm.translated_folders = {} -- set log level for functions @@ -137,14 +141,14 @@ sm.log_level = DEFAULT_LOG_LEVEL --[[ sm.scripts is a table of tables for containing the scripts - It is organized as into category (folder) subtables containing + It is organized as into folder (folder) subtables containing each script definition, which is a table sm.scripts- | - - category------------| + - folder------------| | - script - - category----| | + - folder----| | - script| | - script - script| @@ -152,7 +156,7 @@ sm.log_level = DEFAULT_LOG_LEVEL and a script table looks like name the name of the script file without the lua extension - path category (folder), path separator, path, name without the lua extension + path folder (folder), path separator, path, name without the lua extension doc the header comments from the script to be used as a tooltip script_name the folder, path separator, and name without the lua extension running true if running, false if not, hidden if running but the @@ -174,14 +178,12 @@ sm.log_level = DEFAULT_LOG_LEVEL ]] sm.scripts = {} +sm.start_queue = {} sm.page_status = {} sm.page_status.num_buttons = DEFAULT_BUTTONS_PER_PAGE sm.page_status.buttons_created = 0 sm.page_status.current_page = 0 -sm.page_status.category = "" - --- use color in the interface? -sm.use_color = false +sm.page_status.folder = "" -- installed script repositories sm.installed_repositories = { @@ -212,17 +214,24 @@ end local function pref_read(name, pref_type) local old_log_level = set_log_level(sm.log_level) + log.msg(log.debug, "name is " .. name .. " and type is " .. pref_type) + local val = dt.preferences.read(MODULE, name, pref_type) + log.msg(log.debug, "read value " .. tostring(val)) + restore_log_level(old_log_level) return val end local function pref_write(name, pref_type, value) local old_log_level = set_log_level(sm.log_level) + log.msg(log.debug, "writing value " .. tostring(value) .. " for name " .. name) + dt.preferences.write(MODULE, name, pref_type, value) + restore_log_level(old_log_level) end @@ -232,12 +241,15 @@ end local function get_repo_status(repo) local old_log_level = set_log_level(sm.log_level) + local p = io.popen("cd " .. repo .. CS .. "git status") + if p then local data = p:read("*a") p:close() return data end + log.msg(log.error, "unable to get status of " .. repo) restore_log_level(old_log_level) return nil @@ -245,8 +257,11 @@ end local function get_current_repo_branch(repo) local old_log_level = set_log_level(sm.log_level) + local branch = nil + local p = io.popen("cd " .. repo .. CS .. "git branch --all") + if p then local data = p:read("*a") p:close() @@ -254,23 +269,29 @@ local function get_current_repo_branch(repo) for _, b in ipairs(branches) do log.msg(log.debug, "branch for testing is " .. b) branch = string.match(b, "^%* (.-)$") + if branch then log.msg(log.info, "current repo branch is " .. branch) return branch end + end end + if not branch then log.msg(log.error, "no current branch detected in repo_data") end + restore_log_level(old_log_level) return nil end local function get_repo_branches(repo) local old_log_level = set_log_level(sm.log_level) + local branches = {} local p = io.popen("cd " .. repo .. CS .. "git pull --all" .. CS .. "git branch --all") + if p then local data = p:read("*a") p:close() @@ -285,12 +306,14 @@ local function get_repo_branches(repo) end end end + restore_log_level(old_log_level) return branches end local function is_repo_clean(repo_data) local old_log_level = set_log_level(sm.log_level) + if string.match(repo_data, "\n%s-%a.-%a:%s-%a%g-\n") then log.msg(log.info, "repo is dirty") return false @@ -298,13 +321,17 @@ local function is_repo_clean(repo_data) log.msg(log.info, "repo is clean") return true end + restore_log_level(old_log_level) end local function checkout_repo_branch(repo, branch) local old_log_level = set_log_level(sm.log_level) + log.msg(log.info, "checkout out branch " .. branch .. " from repository " .. repo) + os.execute("cd " .. repo .. CS .. "git checkout " .. branch) + restore_log_level(old_log_level) end @@ -314,16 +341,20 @@ end local function update_combobox_choices(combobox, choice_table, selected) local old_log_level = set_log_level(sm.log_level) + local items = #combobox local choices = #choice_table + for i, name in ipairs(choice_table) do combobox[i] = name end + if choices < items then for j = items, choices + 1, -1 do combobox[j] = nil end end + if not selected then selected = 1 end @@ -334,8 +365,11 @@ end local function string_trim(str) local old_log_level = set_log_level(sm.log_level) - local result = string.gsub(str, "^%s+", "") - result = string.gsub(result, "%s+$", "") + + local result = string.gsub(str, "^%s+", "") -- trim leading spaces + result = string.gsub(result, "%s+$", "") -- trim trailing spaces + result = string.gsub(result, ",?%s+%-%-.+$", "") -- trim trailing comma and comments + restore_log_level(old_log_level) return result end @@ -344,18 +378,108 @@ local function string_dequote(str) return string.gsub(str, "['\"]", "") end +local function string_dei18n(str) + return string.match(str, "%_%((.+)%)") +end + +local function string_chop(str) + return str:sub(1, -2) +end + ------------------ -- script handling ------------------ -local function add_script_category(category) +local function is_folder_known(folder_table, name) + local match = false + + for _, folder_name in ipairs(folder_table) do + if name == folder_name then + match = true + end + end + + return match +end + +local function find_translated_name(folder) + local translated_name = nil + + if folder == "contrib" then + translated_name = _("contributed") + elseif folder == "examples" then + translated_name = _("examples") + elseif folder == "official" then + translated_name = _("official") + elseif folder == "tools" then + translated_name = _("tools") + else + translated_name = _(folder) -- in case we get lucky and the string got translated elsewhere + end + + return translated_name +end + +local function add_script_folder(folder) + local old_log_level = set_log_level(sm.log_level) + + if #sm.folders == 0 or not is_folder_known(sm.folders, folder) then + table.insert(sm.folders, folder) + table.insert(sm.translated_folders, find_translated_name(folder)) + sm.scripts[folder] = {} + log.msg(log.debug, "created folder " .. folder) + end + + restore_log_level(old_log_level) +end + +local function get_script_metadata(script) local old_log_level = set_log_level(sm.log_level) - if #sm.categories == 0 or not string.match(du.join(sm.categories, " "), ds.sanitize_lua(category)) then - table.insert(sm.categories, category) - sm.scripts[category] = {} - log.msg(log.debug, "created category " .. category) + -- set_log_level(log.debug) + + log.msg(log.debug, "processing metatdata for " .. script) + + local metadata_block = nil + local metadata = {} + + f = io.open(LUA_DIR .. PS .. script .. ".lua") + if f then + -- slurp the file + local content = f:read("*all") + f:close() + -- grab the script_data.metadata table + metadata_block = string.match(content, "script_data%.metadata = %{\r?\n(.-)\r?\n%}") + else + log.msg(log.error, "cant read from " .. script) + end + + if metadata_block then + -- break up the lines into key value pairs + local lines = du.split(metadata_block, "\n") + log.msg(log.debug, "got " .. #lines .. " lines") + for i = 1, #lines do + log.msg(log.debug, "splitting line " .. lines[i]) + local parts = du.split(lines[i], " = ") + parts[1] = string_trim(parts[1]) + parts[2] = string_trim(parts[2]) + log.msg(log.debug, "got value " .. parts[1] .. " and data " .. parts[2]) + if string.match(parts[2], "%_%(") then + parts[2] = _(string_dequote(string_dei18n(parts[2]))) + else + parts[2] = string_dequote(parts[2]) + end + if string.match(parts[2], ",$") then + parts[2] = string_chop(parts[2]) + end + log.msg(log.debug, "parts 1 is " .. parts[1] .. " and parts 2 is " .. parts[2]) + metadata[parts[1]] = parts[2] + log.msg(log.debug, "metadata " .. parts[1] .. " is " .. metadata[parts[1]]) + end + log.msg(log.debug, "script data found for " .. metadata["name"]) end + restore_log_level(old_log_level) + return metadata_block and metadata or nil end local function get_script_doc(script) @@ -369,31 +493,39 @@ local function get_script_doc(script) -- assume that the second block comment is the documentation description = string.match(content, "%-%-%[%[.-%]%].-%-%-%[%[(.-)%]%]") else - log.msg(log.error, "Cant read from " .. script) + log.msg(log.error, "can't read from " .. script) end if description then restore_log_level(old_log_level) return description else restore_log_level(old_log_level) - return "No documentation available" + return _("no documentation available") end end local function activate(script) local old_log_level = set_log_level(sm.log_level) + local status = nil -- status of start function local err = nil -- error message returned if module doesn't start + log.msg(log.info, "activating " .. script.name) + if script.running == false then + script_manager_running_script = script.name + status, err = du.prequire(script.path) log.msg(log.debug, "prequire returned " .. tostring(status) .. " and for err " .. tostring(err)) + script_manager_running_script = nil + if status then pref_write(script.script_name, "bool", true) - log.msg(log.screen, string.format(_("loaded %s"), script.script_name)) + log.msg(log.screen, _(string.format("loaded %s", script.script_name))) script.running = true + if err ~= true then log.msg(log.debug, "got lib data") script.data = err @@ -403,17 +535,21 @@ local function activate(script) else script.data = nil end + else - log.msg(log.screen, string.format(_("%s failed to load"), script.script_name)) - log.msg(log.error, "Error loading " .. script.script_name) - log.msg(log.error, "Error message: " .. err) + log.msg(log.screen, _(string.format("%s failed to load", script.script_name))) + log.msg(log.error, "error loading " .. script.script_name) + log.msg(log.error, "error message: " .. err) end + else -- script is a lib and loaded but hidden and the user wants to reload script.data.restart() script.running = true status = true pref_write(script.script_name, "bool", true) end + script_manager_running_script = "script_manager" + restore_log_level(old_log_level) return status end @@ -428,9 +564,13 @@ local function deactivate(script) -- deactivate it.... local old_log_level = set_log_level(sm.log_level) + pref_write(script.script_name, "bool", false) + if script.data then + script.data.destroy() + if script.data.destroy_method then if string.match(script.data.destroy_method, "hide") then script.running = "hidden" @@ -442,44 +582,72 @@ local function deactivate(script) package.loaded[script.script_name] = nil script.running = false end + log.msg(log.info, "turned off " .. script.script_name) - log.msg(log.screen, string.format(_("%s stopped"), script.name)) + log.msg(log.screen, _(string.format("%s stopped", script.name))) + else script.running = false + log.msg(log.info, "setting " .. script.script_name .. " to not start") - log.msg(log.screen, string.format(_("%s will not start when darktable is restarted"), script.name)) + log.msg(log.screen, _(string.format("%s will not start when darktable is restarted", script.name))) end + restore_log_level(old_log_level) end -local function add_script_name(name, path, category) +local function start_scripts() + for _, script in ipairs(sm.start_queue) do + activate(script) + for i = 1, sm.page_status.num_buttons do + local name = script.metadata and script.metadata.name or script.name + if sm.widgets.labels[i].label == name then + sm.widgets.buttons[i].name = "pb_on" + break + end + end + end + sm.start_queue = {} +end + +local function queue_script_to_start(script) + table.insert(sm.start_queue, script) +end + +local function add_script_name(name, path, folder) local old_log_level = set_log_level(sm.log_level) - log.msg(log.debug, "category is " .. category) + + log.msg(log.debug, "folder is " .. folder) log.msg(log.debug, "name is " .. name) + local script = { name = name, - path = category .. "/" .. path .. name, + path = folder .. "/" .. path .. name, running = false, - doc = get_script_doc(category .. "/" .. path .. name), - script_name = category .. "/" .. name, + doc = get_script_doc(folder .. "/" .. path .. name), + metadata = get_script_metadata(folder .. "/" .. path .. name), + script_name = folder .. "/" .. name, data = nil } - table.insert(sm.scripts[category], script) + + table.insert(sm.scripts[folder], script) + if pref_read(script.script_name, "bool") then - activate(script) + queue_script_to_start(script) else pref_write(script.script_name, "bool", false) end + restore_log_level(old_log_level) end local function process_script_data(script_file) local old_log_level = set_log_level(sm.log_level) - -- the script file supplied is category/filename.filetype - -- the following pattern splits the string into category, path, name, fileename, and filetype + -- the script file supplied is folder/filename.filetype + -- the following pattern splits the string into folder, path, name, fileename, and filetype -- for example contrib/gimp.lua becomes - -- category - contrib + -- folder - contrib -- path - -- name - gimp.lua -- filename - gimp @@ -496,14 +664,14 @@ local function process_script_data(script_file) log.msg(log.info, "processing " .. script_file) -- add the script data - local category,path,name,filename,filetype = string.match(script_file, pattern) + local folder,path,name,filename,filetype = string.match(script_file, pattern) - if category and name and path then - log.msg(log.debug, "category is " .. category) + if folder and name and path then + log.msg(log.debug, "folder is " .. folder) log.msg(log.debug, "name is " .. name) - add_script_category(category) - add_script_name(name, path, category) + add_script_folder(folder) + add_script_name(name, path, folder) end restore_log_level(old_log_level) @@ -511,11 +679,10 @@ end local function ensure_lib_in_search_path(line) local old_log_level = set_log_level(sm.log_level) - set_log_level(log.debug) log.msg(log.debug, "line is " .. line) - if string.match(line, ds.sanitize_lua(dt.configuration.config_dir .. PS .. "lua" .. PS .. "lib")) then + if string.match(line, ds.sanitize_lua(dt.configuration.config_dir .. PS .. "lua/lib")) then log.msg(log.debug, line .. " is already in search path, returning...") return end @@ -540,15 +707,20 @@ end local function scan_scripts(script_dir) local old_log_level = set_log_level(sm.log_level) + local script_count = 0 local find_cmd = "find -L " .. script_dir .. " -name \\*.lua -print | sort" + if dt.configuration.running_os == "windows" then find_cmd = "dir /b/s \"" .. script_dir .. "\\*.lua\" | sort" end + log.msg(log.debug, "find command is " .. find_cmd) + -- scan the scripts local output = io.popen(find_cmd) for line in output:lines() do + log.msg(log.debug, "line is " .. line) local l = string.gsub(line, ds.sanitize_lua(LUA_DIR) .. PS, "") -- strip the lua dir off local script_file = l:sub(1,-5) -- strip off .lua\n if not string.match(script_file, "script_manager") then -- let's not include ourself @@ -564,6 +736,7 @@ local function scan_scripts(script_dir) end end end + restore_log_level(old_log_level) return script_count end @@ -575,7 +748,7 @@ local function update_scripts() local git = sm.executables.git if not git then - dt.print(_("ERROR: git not found, install or specify the location of the git executable.")) + log.msg(log.screen, _("ERROR: git not found. Install or specify the location of the git executable.")) return end @@ -589,7 +762,7 @@ local function update_scripts() end if result == 0 then - dt.print(_("lua scripts successfully updated")) + log.msg(log.screen, _("lua scripts successfully updated")) end restore_log_level(old_log_level) @@ -602,65 +775,85 @@ end local function update_script_update_choices() local old_log_level = set_log_level(sm.log_level) + local installs = {} local pref_string = "" + for i, repo in ipairs(sm.installed_repositories) do table.insert(installs, repo.name) pref_string = pref_string .. i .. "," .. repo.name .. "," .. repo.directory .. "," end + update_combobox_choices(sm.widgets.update_script_choices, installs, 1) + log.msg(log.debug, "repo pref string is " .. pref_string) pref_write("installed_repos", "string", pref_string) + restore_log_level(old_log_level) end local function scan_repositories() local old_log_level = set_log_level(sm.log_level) + local script_count = 0 local find_cmd = "find -L " .. LUA_DIR .. " -name \\*.git -print | sort" + if dt.configuration.running_os == "windows" then find_cmd = "dir /b/s /a:d " .. LUA_DIR .. PS .. "*.git | sort" end + log.msg(log.debug, "find command is " .. find_cmd) + local output = io.popen(find_cmd) + for line in output:lines() do local l = string.gsub(line, ds.sanitize_lua(LUA_DIR) .. PS, "") -- strip the lua dir off - local category = string.match(l, "(.-)" .. PS) -- get everything to teh first / - if category then -- if we have a category (.git doesn't) - log.msg(log.debug, "found category " .. category) - if not string.match(category, "plugins") and not string.match(category, "%.git") then -- skip plugins + local folder = string.match(l, "(.-)" .. PS) -- get everything to the first / + + if folder then -- if we have a folder (.git doesn't) + + log.msg(log.debug, "found folder " .. folder) + + if not string.match(folder, "plugins") and not string.match(folder, "%.git") then -- skip plugins + if #sm.installed_repositories == 1 then - log.msg(log.debug, "only 1 repo, adding " .. category) - table.insert(sm.installed_repositories, {name = category, directory = LUA_DIR .. PS .. category}) + log.msg(log.debug, "only 1 repo, adding " .. folder) + table.insert(sm.installed_repositories, {name = folder, directory = LUA_DIR .. PS .. folder}) else log.msg(log.debug, "more than 1 repo, we have to search the repos to make sure it's not there") local found = nil + for _, repo in ipairs(sm.installed_repositories) do - if string.match(repo.name, ds.sanitize_lua(category)) then + if string.match(repo.name, ds.sanitize_lua(folder)) then log.msg(log.debug, "matched " .. repo.name) found = true break end end + if not found then - table.insert(sm.installed_repositories, {name = category, directory = LUA_DIR .. PS .. category}) + table.insert(sm.installed_repositories, {name = folder, directory = LUA_DIR .. PS .. folder}) end + end end end end + update_script_update_choices() + restore_log_level(old_log_level) end local function install_scripts() local old_log_level = set_log_level(sm.log_level) + local url = sm.widgets.script_url.text - local category = sm.widgets.new_category.text + local folder = sm.widgets.new_folder.text - if string.match(du.join(sm.categories, " "), ds.sanitize_lua(category)) then - log.msg(log.screen, string.format(_("category %s is already in use, please specify a different category name."), category)) - log.msg(log.error, "category " .. category .. " already exists, returning...") + if string.match(du.join(sm.folders, " "), ds.sanitize_lua(folder)) then + log.msg(log.screen, _(string.format("folder %s is already in use. Please specify a different folder name.", folder))) + log.msg(log.error, "folder " .. folder .. " already exists, returning...") restore_log_level(old_log_level) return end @@ -670,12 +863,12 @@ local function install_scripts() local git = sm.executables.git if not git then - dt.print(_("ERROR: git not found, install or specify the location of the git executable.")) + log.msg(log.screen, _("ERROR: git not found. Install or specify the location of the git executable.")) restore_log_level(old_log_level) return end - local git_command = "cd " .. LUA_DIR .. " " .. CS .. " " .. git .. " clone " .. url .. " " .. category + local git_command = "cd " .. LUA_DIR .. " " .. CS .. " " .. git .. " clone " .. url .. " " .. folder log.msg(log.debug, "update git command is " .. git_command) if dt.configuration.running_os == "windows" then @@ -687,30 +880,34 @@ local function install_scripts() log.msg(log.info, "result from import is " .. result) if result == 0 then - local count = scan_scripts(LUA_DIR .. PS .. category) + local count = scan_scripts(LUA_DIR .. PS .. folder) + if count > 0 then - update_combobox_choices(sm.widgets.category_selector, sm.categories, sm.widgets.category_selector.selected) - dt.print(string.format(_("scripts successfully installed into category "), category)) - table.insert(sm.installed_repositories, {name = category, directory = LUA_DIR .. PS .. category}) + update_combobox_choices(sm.widgets.folder_selector, sm.folders, sm.widgets.folder_selector.selected) + dt.print(_(string.format("scripts successfully installed into folder %s"), folder)) + table.insert(sm.installed_repositories, {name = folder, directory = LUA_DIR .. PS .. folder}) update_script_update_choices() - for i = 1, #sm.widgets.category_selector do - if string.match(sm.widgets.category_selector[i], ds.sanitize_lua(category)) then - log.msg(log.debug, "setting category selector to " .. i) - sm.widgets.category_selector.selected = i + + for i = 1, #sm.widgets.folder_selector do + if string.match(sm.widgets.folder_selector[i], ds.sanitize_lua(folder)) then + log.msg(log.debug, "setting folder selector to " .. i) + sm.widgets.folder_selector.selected = i break end i = i + 1 end + log.msg(log.debug, "clearing text fields") sm.widgets.script_url.text = "" - sm.widgets.new_category.text = "" + sm.widgets.new_folder.text = "" sm.widgets.main_menu.selected = 3 else - dt.print(_("no scripts found to install")) - log.msg(log.error, "scan_scripts returned " .. count .. " scripts found. Not adding to category_selector") + log.msg(log.screen, _("no scripts found to install")) + log.msg(log.error, "scan_scripts returned " .. count .. " scripts found. Not adding to folder_selector") end + else - dt.print(_("failed to download scripts")) + log.msg(log.screen, _("failed to download scripts")) end restore_log_level(old_log_level) @@ -719,102 +916,101 @@ end local function clear_button(number) local old_log_level = set_log_level(sm.log_level) + local button = sm.widgets.buttons[number] - button.label = "" + local label = sm.widgets.labels[number] + + button.image = BLANK_ICON button.tooltip = "" button.sensitive = false - --button.name = "" + label.label = "" + button.name = "" + restore_log_level(old_log_level) end -local function find_script(category, name) +local function find_script(folder, name) local old_log_level = set_log_level(sm.log_level) - log.msg(log.debug, "looking for script " .. name .. " in category " .. category) - for _, script in ipairs(sm.scripts[category]) do + + log.msg(log.debug, "looking for script " .. name .. " in folder " .. folder) + + for _, script in ipairs(sm.scripts[folder]) do if string.match(script.name, "^" .. ds.sanitize_lua(name) .. "$") then return script end end + restore_log_level(old_log_level) return nil end -local function populate_buttons(category, first, last) +local function populate_buttons(folder, first, last) local old_log_level = set_log_level(sm.log_level) - log.msg(log.debug, "category is " .. category .. " and first is " .. first .. " and last is " .. last) + + log.msg(log.debug, "folder is " .. folder .. " and first is " .. first .. " and last is " .. last) + local button_num = 1 + for i = first, last do - script = sm.scripts[category][i] - button = sm.widgets.buttons[button_num] + local script = sm.scripts[folder][i] + local button = sm.widgets.buttons[button_num] + local label = sm.widgets.labels[button_num] + if script.running == true then - if sm.use_color then - button.label = script.name - button.name = "sm_started" - else - button.label = string.format(_("%s started"), script.name) - end + button.name = "pb_on" else - if sm.use_color then - button.label = script.name - button.name = "sm_stopped" - else - button.label = string.format(_("%s stopped"), script.name) - end + button.name = "pb_off" end - button.ellipsize = "middle" + + button.image = POWER_ICON + label.label = script.metadata and script.metadata.name or script.name + label.name = "pb_label" + button.ellipsize = "end" button.sensitive = true - button.tooltip = script.doc + label.tooltip = script.metadata and script.metadata.purpose or script.doc + button.clicked_callback = function (this) - local script_name = nil + local cb_script = script local state = nil - if sm.use_color then - script_name = string.match(this.label, "(.+)") - else - script_name, state = string.match(this.label, "(.-) (.+)") - end - local script = find_script(sm.widgets.category_selector.value, script_name) - if script then - log.msg(log.debug, "found script " .. script.name .. " with path " .. script.path) - if script.running == true then - log.msg(log.debug, "deactivating " .. script.name .. " on " .. script.path .. " for button " .. this.label) - deactivate(script) - if sm.use_color then - this.name = "sm_stopped" - else - this.label = string.format(_("%s stopped"), script.name) - end + if cb_script then + log.msg(log.debug, "found script " .. cb_script.name .. " with path " .. cb_script.path) + if cb_script.running == true then + log.msg(log.debug, "deactivating " .. cb_script.name .. " on " .. cb_script.path) + deactivate(cb_script) + this.name = "pb_off" else - log.msg(log.debug, "activating " .. script.name .. " on " .. script.path .. " for button " .. this.label) - local result = activate(script) + log.msg(log.debug, "activating " .. cb_script.name .. " on " .. script.path) + local result = activate(cb_script) if result then - if sm.use_color then - this.name = "sm_started" - else - this.label = string.format(_("%s started"), script.name) - end + this.name = "pb_on" end end - else - log.msg(log.error, "script " .. script_name .. " not found") end end + button_num = button_num + 1 end + if button_num <= sm.page_status.num_buttons then for i = button_num, sm.page_status.num_buttons do clear_button(i) end end + restore_log_level(old_log_level) end local function paginate(direction) local old_log_level = set_log_level(sm.log_level) - local category = sm.page_status.category - log.msg(log.debug, "category is " .. category) - local num_scripts = #sm.scripts[category] + + local folder = sm.page_status.folder + log.msg(log.debug, "folder is " .. folder) + + local num_scripts = #sm.scripts[folder] log.msg(log.debug, "num_scripts is " .. num_scripts) + local max_pages = math.ceil(num_scripts / sm.page_status.num_buttons) + local cur_page = sm.page_status.current_page log.msg(log.debug, "max pages is " .. max_pages) @@ -836,7 +1032,9 @@ local function paginate(direction) log.msg(log.debug, "took path 2") cur_page = 1 end + log.msg(log.debug, "cur_page is " .. cur_page .. " and max_pages is " .. max_pages) + if cur_page == max_pages and cur_page == 1 then sm.widgets.page_forward.sensitive = false sm.widgets.page_back.sensitive = false @@ -852,119 +1050,171 @@ local function paginate(direction) end sm.page_status.current_page = cur_page + first = (cur_page * sm.page_status.num_buttons) - (sm.page_status.num_buttons - 1) + if first + sm.page_status.num_buttons > num_scripts then last = num_scripts else last = first + sm.page_status.num_buttons - 1 end + sm.widgets.page_status.label = string.format(_("page %d of %d"), cur_page, max_pages) - populate_buttons(category, first, last) + populate_buttons(folder, first, last) + restore_log_level(old_log_level) end -local function change_category(category) +local function change_folder(folder) local old_log_level = set_log_level(sm.log_level) - if not category then - log.msg(log.debug "setting category to selector value " .. sm.widgets.category_selector.value) - sm.page_status.category = sm.widgets.category_selector.value + + if not folder then + log.msg(log.debug "setting folder to selector value " .. sm.widgets.folder_selector.value) + sm.page_status.folder = sm.widgets.folder_selector.value else - log.msg(log.debug, "setting catgory to argument " .. category) - sm.page_status.category = category + log.msg(log.debug, "setting catgory to argument " .. folder) + sm.page_status.folder = folder end paginate(2) + restore_log_level(old_log_level) end local function change_num_buttons() local old_log_level = set_log_level(sm.log_level) + cur_buttons = sm.page_status.num_buttons new_buttons = sm.widgets.num_buttons.value + pref_write("num_buttons", "integer", new_buttons) + if new_buttons < cur_buttons then + log.msg(log.debug, "took new is less than current branch") + for i = 1, cur_buttons - new_buttons do table.remove(sm.widgets.scripts) end + log.msg(log.debug, "finished removing widgets, now there are " .. #sm.widgets.buttons) elseif new_buttons > cur_buttons then + log.msg(log.debug, "took new is greater than current branch") + log.msg(log.debug, "number of scripts is " .. #sm.widgets.scripts) + log.msg(log.debug, "number of buttons is " .. #sm.widgets.buttons) + log.msg(log.debug, "number of labels is " .. #sm.widgets.labels) + log.msg(log.debug, "number of boxes is " .. #sm.widgets.boxes) + if new_buttons > sm.page_status.buttons_created then + for i = sm.page_status.buttons_created + 1, new_buttons do + log.msg(log.debug, "i is " .. i) table.insert(sm.widgets.buttons, dt.new_widget("button"){}) + log.msg(log.debug, "inserted new button") + log.msg(log.debug, "number of buttons is " .. #sm.widgets.buttons) + table.insert(sm.widgets.labels, dt.new_widget("label"){}) + log.msg(log.debug, "inserted new label") + log.msg(log.debug, "number of labels is " .. #sm.widgets.labels) + table.insert(sm.widgets.boxes, dt.new_widget("box"){ orientation = "horizontal", expand = false, fill = false, + sm.widgets.buttons[i], sm.widgets.labels[i]}) + log.msg(log.debug, "inserted new box") sm.page_status.buttons_created = sm.page_status.buttons_created + 1 end + end + log.msg(log.debug, "cur_buttons is " .. cur_buttons .. " and new_buttons is " .. new_buttons) log.msg(log.debug, #sm.widgets.buttons .. " buttons are available") + for i = cur_buttons + 1, new_buttons do log.msg(log.debug, "inserting button " .. i .. " into scripts widget") - table.insert(sm.widgets.scripts, sm.widgets.buttons[i]) + table.insert(sm.widgets.scripts, sm.widgets.boxes[i]) end + log.msg(log.debug, "finished adding widgets, now there are " .. #sm.widgets.buttons) else -- no change log.msg(log.debug, "no change, just returning") return end + sm.page_status.num_buttons = new_buttons log.msg(log.debug, "num_buttons set to " .. sm.page_status.num_buttons) paginate(2) -- force the buttons to repopulate sm.widgets.main_menu.selected = 3 -- jump back to start/stop scripts + restore_log_level(old_log_level) end local function load_preferences() local old_log_level = set_log_level(sm.log_level) + -- load the prefs and update settings -- update_script_choices + local pref_string = pref_read("installed_repos", "string") local entries = du.split(pref_string, ",") + while #entries > 2 do local num = table.remove(entries, 1) local name = table.remove(entries, 1) local directory = table.remove(entries, 1) + if not string.match(sm.installed_repositories[1].name, "^" .. ds.sanitize_lua(name) .. "$") then table.insert(sm.installed_repositories, {name = name, directory = directory}) end + end + update_script_update_choices() log.msg(log.debug, "updated installed scripts") - -- category selector - local val = pref_read("category_selector", "integer") + + -- folder selector + local val = pref_read("folder_selector", "integer") + if val == 0 then val = 1 end - sm.widgets.category_selector.selected = val - sm.page_status.category = sm.widgets.category_selector.value - log.msg(log.debug, "updated category selector and set it to " .. sm.widgets.category_selector.value) + + sm.widgets.folder_selector.selected = val + sm.page_status.folder = sm.widgets.folder_selector.value + log.msg(log.debug, "updated folder selector and set it to " .. sm.widgets.folder_selector.value) + -- num_buttons local val = pref_read("num_buttons", "integer") + if val == 0 then val = DEFAULT_BUTTONS_PER_PAGE end + sm.widgets.num_buttons.value = val log.msg(log.debug, "set page buttons to " .. val) + change_num_buttons() log.msg(log.debug, "paginated") + -- main menu local val = pref_read("main_menu_action", "integer") log.msg(log.debug, "read " .. val .. " for main menu") + if val == 0 then val = 3 end + sm.widgets.main_menu.selected = val log.msg(log.debug, "set main menu to val " .. val .. " which is " .. sm.widgets.main_menu.value) log.msg(log.debug, "set main menu to " .. sm.widgets.main_menu.value) + restore_log_level(old_log_level) end local function install_module() local old_log_level = set_log_level(sm.log_level) + if not sm.module_installed then dt.register_lib( "script_manager", -- Module name - "script manager", -- Visible name + _("scripts"), -- Visible name true, -- expandable false, -- resetable {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_BOTTOM", 100}}, -- containers @@ -974,22 +1224,14 @@ local function install_module() ) sm.module_installed = true end + sm.run = true sm.use_color = pref_read("use_color", "bool") log.msg(log.debug, "set run to true, loading preferences") load_preferences() scan_repositories() - --[[dt.print_log("\n\nsetting sm visible false\n\n") - dt.gui.libs["script_manager"].visible = false - dt.control.sleep(5000) - dt.print_log("setting sm visible true") - dt.gui.libs["script_manager"].visible = true - --[[dt.control.sleep(5000) - dt.print_log("setting sm expanded false") - dt.gui.libs["script_manager"].expanded = false - dt.control.sleep(5000) - dt.print_log("setting sm expanded true") - dt.gui.libs["script_manager"].expanded = true]] + start_scripts() + restore_log_level(old_log_level) end @@ -1001,29 +1243,35 @@ end script_manager_running_script = "script_manager" -if check_for_updates then +if check_for_updates or SM_API_VER_REQD > dt.configuration.api_version_string then local repo_data = get_repo_status(LUA_DIR) local current_branch = get_current_repo_branch(LUA_DIR) local clean = is_repo_clean(repo_data) local repo = LUA_DIR if current_branch then + if sm.executables.git and clean and + (current_branch == "master" or string.match(current_branch, "^API%-")) then -- only make changes to clean branches local branches = get_repo_branches(LUA_DIR) + if current_branch ~= LUA_API_VER and current_branch ~= "master" then -- probably upgraded from an earlier api version so get back to master -- to use the latest version of script_manager to get the proper API checkout_repo_branch(repo, "master") - log.msg(log.screen, "lua API version reset, please restart darktable") + log.msg(log.screen, _("lua API version reset, please restart darktable")) + elseif LUA_API_VER == current_branch then -- do nothing, we are fine log.msg(log.debug, "took equal branch, doing nothing") + elseif string.match(LUA_API_VER, "dev") then -- we are on a dev API version, so checkout the dev -- api version or checkout/stay on master log.msg(log.debug, "took the dev branch") local match = false + for _, branch in ipairs(branches) do log.msg(log.debug, "checking branch " .. branch .. " against API " .. LUA_API_VER) if LUA_API_VER == branch then @@ -1032,6 +1280,7 @@ if check_for_updates then checkout_repo_branch(repo, branch) end end + if not match then if current_branch == "master" then log.msg(log.info, "staying on master, no dev branch yet") @@ -1040,25 +1289,34 @@ if check_for_updates then checkout_repo_branch(repo, "master") end end + elseif #branches > 0 and LUA_API_VER > branches[#branches] then log.msg(log.info, "no newer branches, staying on master") -- stay on master + else -- checkout the appropriate branch for API version if it exists log.msg(log.info, "checking out the appropriate API branch") + local match = false - for _, branch in ipairs(branches) do + + for _x, branch in ipairs(branches) do log.msg(log.debug, "checking branch " .. branch .. " against API " .. LUA_API_VER) + if LUA_API_VER == branch then match = true log.msg(log.info, "checking out repo branch " .. branch) checkout_repo_branch(repo, branch) - log.msg(log.screen, "you must restart darktable to use the correct version of the lua") + log.msg(log.screen, _("you must restart darktable to use the correct version of the lua scripts")) + return end + end + if not match then log.msg(log.warn, "no matching branch found for " .. LUA_API_VER) end + end end end @@ -1100,18 +1358,18 @@ sm.widgets.script_url = dt.new_widget("entry"){ tooltip = _("enter the URL of the git repository containing the scripts you wish to add") } -sm.widgets.new_category = dt.new_widget("entry"){ +sm.widgets.new_folder = dt.new_widget("entry"){ text = "", - placeholder = _("name of new category"), - tooltip = _("enter a category name for the additional scripts") + placeholder = _("name of new folder"), + tooltip = _("enter a folder name for the additional scripts") } sm.widgets.add_scripts = dt.new_widget("box"){ orientation = vertical, dt.new_widget("label"){label = _("URL to download additional scripts from")}, sm.widgets.script_url, - dt.new_widget("label"){label = _("new category to place scripts in")}, - sm.widgets.new_category, + dt.new_widget("label"){label = _("new folder to place scripts in")}, + sm.widgets.new_folder, dt.new_widget("button"){ label = _("install additional scripts"), clicked_callback = function(this) @@ -1126,6 +1384,8 @@ sm.widgets.allow_disable = dt.new_widget("check_button"){ clicked_callback = function(this) if this.value == true then sm.widgets.disable_scripts.sensitive = true + else + sm.widgets.disable_scripts.sensitive = false end end, } @@ -1137,47 +1397,69 @@ sm.widgets.disable_scripts = dt.new_widget("button"){ local LUARC = dt.configuration.config_dir .. PS .. "luarc" df.file_move(LUARC, LUARC .. ".disabled") log.msg(log.info, "lua scripts disabled") - dt.print(_("lua scripts will not run the next time darktable is started")) + log.msg(log.screen, _("lua scripts will not run the next time darktable is started")) end } sm.widgets.install_update = dt.new_widget("box"){ orientation = "vertical", - dt.new_widget("section_label"){label = _("update scripts")}, + dt.new_widget("section_label"){label = " "}, + dt.new_widget("label"){label = " "}, + dt.new_widget("label"){label = _("update scripts")}, + dt.new_widget("label"){label = " "}, sm.widgets.update_script_choices, sm.widgets.update, - dt.new_widget("section_label"){label = _("add more scripts")}, + dt.new_widget("section_label"){label = " "}, + dt.new_widget("label"){label = " "}, + dt.new_widget("label"){label = _("add more scripts")}, + dt.new_widget("label"){label = " "}, sm.widgets.add_scripts, - dt.new_widget("section_label"){label = _("disable scripts")}, + dt.new_widget("section_label"){label = " "}, + dt.new_widget("label"){label = " "}, + dt.new_widget("label"){label = _("disable scripts")}, + dt.new_widget("label"){label = " "}, sm.widgets.allow_disable, - sm.widgets.disable_scripts + sm.widgets.disable_scripts, + dt.new_widget("label"){label = " "}, } -- manage the scripts -sm.widgets.category_selector = dt.new_widget("combobox"){ - label = _("category"), - tooltip = _("select the script category"), +sm.widgets.folder_selector = dt.new_widget("combobox"){ + label = _("folder"), + tooltip = _( "select the script folder"), selected = 1, changed_callback = function(self) if sm.run then - pref_write("category_selector", "integer", self.selected) - change_category(self.value) + pref_write("folder_selector", "integer", self.selected) + change_folder(sm.folders[self.selected]) end end, - table.unpack(sm.categories), + table.unpack(sm.translated_folders), } +-- a script "button" consists of: +-- a button to start and stop the script +-- a label that contains the name of the script +-- a horizontal box that contains the button and the label + sm.widgets.buttons ={} +sm.widgets.labels = {} +sm.widgets.boxes = {} + for i =1, DEFAULT_BUTTONS_PER_PAGE do table.insert(sm.widgets.buttons, dt.new_widget("button"){}) - sm.page_status.buttons_create = sm.page_status.buttons_created + 1 + table.insert(sm.widgets.labels, dt.new_widget("label"){}) + table.insert(sm.widgets.boxes, dt.new_widget("box"){ orientation = "horizontal", expand = false, fill = false, + sm.widgets.buttons[i], sm.widgets.labels[i]}) + sm.page_status.buttons_created = sm.page_status.buttons_created + 1 end local page_back = "<" local page_forward = ">" -sm.widgets.page_status = dt.new_widget("label"){label = _("page:")} +sm.widgets.page_status = dt.new_widget("label"){label = _("page") .. ":"} + sm.widgets.page_back = dt.new_widget("button"){ label = page_back, clicked_callback = function(this) @@ -1205,10 +1487,12 @@ sm.widgets.page_control = dt.new_widget("box"){ sm.widgets.scripts = dt.new_widget("box"){ orientation = vertical, + dt.new_widget("section_label"){label = " "}, + dt.new_widget("label"){label = " "}, dt.new_widget("label"){label = _("scripts")}, - sm.widgets.category_selector, + sm.widgets.folder_selector, sm.widgets.page_control, - table.unpack(sm.widgets.buttons) + table.unpack(sm.widgets.boxes), } -- configure options @@ -1234,21 +1518,16 @@ sm.widgets.change_buttons = dt.new_widget("button"){ sm.widgets.configure = dt.new_widget("box"){ orientation = "vertical", + dt.new_widget("section_label"){label = " "}, + dt.new_widget("label"){label = " "}, dt.new_widget("label"){label = _("configuration")}, + dt.new_widget("label"){label = " "}, sm.widgets.num_buttons, + dt.new_widget("label"){label = " "}, sm.widgets.change_buttons, + dt.new_widget("label"){label = " "}, } -sm.widgets.color = dt.new_widget("check_button"){ - label = _("use color interface?"), - value = pref_read("use_color", "bool"), - clicked_callback = function(this) - pref_write("use_color", "bool", this.value) - sm.use_color = this.value - end -} -table.insert(sm.widgets.configure, sm.widgets.color) - -- stack for the options sm.widgets.main_stack = dt.new_widget("stack"){ @@ -1294,7 +1573,7 @@ else function(event, old_view, new_view) if new_view.name == "lighttable" and old_view.name == "darkroom" then install_module() - end + end end ) sm.event_registered = true