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 c89804d8..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,42 +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_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 @@ -77,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 @@ -95,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 @@ -117,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 @@ -174,7 +186,7 @@ The recommended way to enable and disable specific scripts is using the script m ### Windows - echo "require 'tools/script_manager'" > %LOCALAPPDATA%\darktable\luarc + echo require "tools/script_manager" > %LOCALAPPDATA%\darktable\luarc ### Snap @@ -182,7 +194,7 @@ The recommended way to enable and disable specific scripts is using the script m ### Flatpak - echo require "tools/script_manager"' > ~/.var/app/org.darktable.Darktable/config/darktable/luarc + echo 'require "tools/script_manager"' > ~/.var/app/org.darktable.Darktable/config/darktable/luarc You can also create or add lines to the luarc file from the command line: @@ -225,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 58a8c1b8..889c9af1 100644 --- a/contrib/AutoGrouper.lua +++ b/contrib/AutoGrouper.lua @@ -1,218 +1,225 @@ ---[[AutoGrouper plugin for darktable - - copyright (c) 2019 Kevin Ertel - - 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 . -]] - ---[[About this Plugin -This plugin adds the module "Auto Group" to darktable's lighttable view - -----REQUIRED SOFTWARE---- -None - -----USAGE---- -Install: (see here for more detail: https://github.com/darktable-org/lua-scripts ) - 1) Copy this file in to your "lua/contrib" folder where all other scripts reside. - 2) Require this file in your luarc file, as with any other dt plug-in - -Set a gap amount in second which will be used to determine when images should no -longer be added to a group. If an image is more then the specified amount of time -from the last image in the group it will not be added. Images without timestamps -in exif data will be ignored. - -There are two buttons. One allows the grouping to be performed only on the currently -selected images, the other button performs grouping on the entire active collection -]] - -local dt = require "darktable" -local du = require "lib/dtutils" - -du.check_min_api_version("7.0.0", "AutoGrouper") - -local MOD = 'autogrouper' - --- return data structure for script_manager - -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, otherwise leave as nil -script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - -local gettext = dt.gettext --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("AutoGrouper",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("AutoGrouper", msgid) -end - -local Ag = {} -Ag.module_installed = false -Ag.event_registered = false - -local GUI = { - gap = {}, - selected = {}, - collection = {} -} - - -local function InRange(test, low, high) --tests if test value is within range of low and high (inclusive) - if test >= low and test <= high then - return true - else - return false - end -end - -local function CompTime(first, second) --compares the timestamps and returns true if first was taken before second - first_time = first.exif_datetime_taken - if string.match(first_time, '[0-9]') == nil then first_time = '9999:99:99 99:99:99' end - first_time = tonumber(string.gsub(first_time, '[^0-9]*','')) - second_time = second.exif_datetime_taken - if string.match(second_time, '[0-9]') == nil then second_time = '9999:99:99 99:99:99' end - second_time = tonumber(string.gsub(second_time, '[^0-9]*','')) - return first_time < second_time -end - -local function SeperateTime(str) --seperates the timestamp into individual components for used with OS.time operations - local cleaned = string.gsub(str, '[^%d]',':') - cleaned = string.gsub(cleaned, '::*',':') --YYYY:MM:DD:hh:mm:ss - local year = string.sub(cleaned,1,4) - local month = string.sub(cleaned,6,7) - local day = string.sub(cleaned,9,10) - local hour = string.sub(cleaned,12,13) - local min = string.sub(cleaned,15,16) - local sec = string.sub(cleaned,18,19) - return {year = year, month = month, day = day, hour = hour, min = min, sec = sec} -end - -local function GetTimeDiff(curr_image, prev_image) --returns the time difference (in sec.) from current image and the previous image - local curr_time = SeperateTime(curr_image.exif_datetime_taken) - local prev_time = SeperateTime(prev_image.exif_datetime_taken) - return os.time(curr_time)-os.time(prev_time) -end - -local function main(on_collection) - local images = {} - if on_collection then - local col_images = dt.collection - for i,image in ipairs(col_images) do --copy images to a standard table, table.sort barfs on type dt_lua_singleton_image_collection - table.insert(images,i,image) - end - else - images = dt.gui.selection() - end - dt.preferences.write(MOD, 'active_gap', 'integer', GUI.gap.value) - if #images < 2 then - dt.print('please select at least 2 images') - return - end - table.sort(images, function(first, second) return CompTime(first,second) end) --sort images by timestamp - - for i, image in ipairs(images) do - if i == 1 then - prev_image = image - 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]) - end - prev_image = curr_image - end - end -end - -local function install_module() - if not Ag.module_installed then - dt.print_log("installing module") - dt.register_lib( - MOD, -- Module name - _('auto group'), -- name - true, -- expandable - true, -- resetable - {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 99}}, -- containers - dt.new_widget("box"){ - orientation = "vertical", - GUI.gap, - GUI.selected, - GUI.collection - } - ) - Ag.module_installed = true - dt.print_log("module installed") - dt.print_log("styles module visibility is " .. tostring(dt.gui.libs["styles"].visible)) - end -end - -local function destroy() - dt.gui.libs[MOD].visible = false -end - -local function restart() - dt.gui.libs[MOD].visible = true -end - --- GUI -- -temp = dt.preferences.read(MOD, 'active_gap', 'integer') -if not InRange(temp, 1, 86400) then temp = 3 end -GUI.gap = dt.new_widget('slider'){ - label = _('group gap [sec.]'), - tooltip = _('minimum gap, in seconds, between groups'), - soft_min = 1, - soft_max = 60, - hard_min = 1, - hard_max = 86400, - step = 1, - digits = 0, - value = temp, - reset_callback = function(self) - self.value = 3 - end -} -GUI.selected = dt.new_widget("button"){ - label = _('auto group: selected'), - tooltip =_('auto group selected images'), - clicked_callback = function() main(false) end -} -GUI.collection = dt.new_widget("button"){ - label = _('auto group: collection'), - tooltip =_('auto group the entire collection'), - clicked_callback = function() main(true) end -} - -if dt.gui.current_view().id == "lighttable" then - install_module() -else - if not Ag.event_registered then - dt.register_event( - "AutoGrouper", "view-changed", - function(event, old_view, new_view) - if new_view.name == "lighttable" and old_view.name == "darkroom" then - install_module() - end - end - ) - Ag.event_registered = true - end -end - -script_data.destroy = destroy -script_data.destroy_method = "hide" -script_data.restart = restart -script_data.show = restart - +--[[AutoGrouper plugin for darktable + + copyright (c) 2019 Kevin Ertel + + 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 . +]] + +--[[About this Plugin +This plugin adds the module "Auto Group" to darktable's lighttable view + +----REQUIRED SOFTWARE---- +None + +----USAGE---- +Install: (see here for more detail: https://github.com/darktable-org/lua-scripts ) + 1) Copy this file in to your "lua/contrib" folder where all other scripts reside. + 2) Require this file in your luarc file, as with any other dt plug-in + +Set a gap amount in second which will be used to determine when images should no +longer be added to a group. If an image is more then the specified amount of time +from the last image in the group it will not be added. Images without timestamps +in exif data will be ignored. + +There are two buttons. One allows the grouping to be performed only on the currently +selected images, the other button performs grouping on the entire active collection +]] + +local dt = require "darktable" +local du = require "lib/dtutils" + +du.check_min_api_version("7.0.0", "AutoGrouper") + +local MOD = 'autogrouper' + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + 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/" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +local Ag = {} +Ag.module_installed = false +Ag.event_registered = false + +local GUI = { + gap = {}, + selected = {}, + collection = {} +} + + +local function InRange(test, low, high) --tests if test value is within range of low and high (inclusive) + if test >= low and test <= high then + return true + else + return false + end +end + +local function CompTime(first, second) --compares the timestamps and returns true if first was taken before second + first_time = first.exif_datetime_taken + if string.match(first_time, '[0-9]') == nil then first_time = '9999:99:99 99:99:99' end + first_time = tonumber(string.gsub(first_time, '[^0-9]*','')) + second_time = second.exif_datetime_taken + if string.match(second_time, '[0-9]') == nil then second_time = '9999:99:99 99:99:99' end + second_time = tonumber(string.gsub(second_time, '[^0-9]*','')) + return first_time < second_time +end + +local function SeperateTime(str) --seperates the timestamp into individual components for used with OS.time operations + local cleaned = string.gsub(str, '[^%d]',':') + cleaned = string.gsub(cleaned, '::*',':') --YYYY:MM:DD:hh:mm:ss + local year = string.sub(cleaned,1,4) + local month = string.sub(cleaned,6,7) + local day = string.sub(cleaned,9,10) + local hour = string.sub(cleaned,12,13) + local min = string.sub(cleaned,15,16) + local sec = string.sub(cleaned,18,19) + return {year = year, month = month, day = day, hour = hour, min = min, sec = sec} +end + +local function GetTimeDiff(curr_image, prev_image) --returns the time difference (in sec.) from current image and the previous image + local curr_time = SeperateTime(curr_image.exif_datetime_taken) + local prev_time = SeperateTime(prev_image.exif_datetime_taken) + return os.time(curr_time)-os.time(prev_time) +end + +local function main(on_collection) + local images = {} + if on_collection then + local col_images = dt.collection + for i,image in ipairs(col_images) do --copy images to a standard table, table.sort barfs on type dt_lua_singleton_image_collection + table.insert(images,i,image) + end + else + images = dt.gui.selection() + end + dt.preferences.write(MOD, 'active_gap', 'integer', GUI.gap.value) + if #images < 2 then + dt.print('please select at least 2 images') + return + end + table.sort(images, function(first, second) return CompTime(first,second) end) --sort images by timestamp + + 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].group_leader) + end + prev_image = curr_image + end + end +end + +local function install_module() + if not Ag.module_installed then + dt.print_log("installing module") + dt.register_lib( + MOD, -- Module name + _('auto group'), -- name + true, -- expandable + true, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 99}}, -- containers + dt.new_widget("box"){ + orientation = "vertical", + GUI.gap, + GUI.selected, + GUI.collection + } + ) + Ag.module_installed = true + dt.print_log("module installed") + dt.print_log("styles module visibility is " .. tostring(dt.gui.libs["styles"].visible)) + end +end + +local function destroy() + dt.gui.libs[MOD].visible = false +end + +local function restart() + dt.gui.libs[MOD].visible = true +end + +-- GUI -- +temp = dt.preferences.read(MOD, 'active_gap', 'integer') +if not InRange(temp, 1, 86400) then temp = 3 end +GUI.gap = dt.new_widget('slider'){ + label = _('group gap [sec.]'), + tooltip = _('minimum gap, in seconds, between groups'), + soft_min = 1, + soft_max = 60, + hard_min = 1, + hard_max = 86400, + step = 1, + digits = 0, + value = temp, + reset_callback = function(self) + self.value = 3 + end +} +GUI.selected = dt.new_widget("button"){ + label = _('auto group: selected'), + tooltip =_('auto group selected images'), + clicked_callback = function() main(false) end +} +GUI.collection = dt.new_widget("button"){ + label = _('auto group: collection'), + tooltip =_('auto group the entire collection'), + clicked_callback = function() main(true) end +} + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not Ag.event_registered then + dt.register_event( + "AutoGrouper", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + Ag.event_registered = true + end +end + +script_data.destroy = destroy +script_data.destroy_method = "hide" +script_data.restart = restart +script_data.show = restart + return script_data diff --git a/contrib/CollectHelper.lua b/contrib/CollectHelper.lua index 94c5f77a..978aa309 100644 --- a/contrib/CollectHelper.lua +++ b/contrib/CollectHelper.lua @@ -1,253 +1,258 @@ ---[[Collect Helper plugin for darktable - - copyright (c) 2019 Kevin Ertel - - 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 . -]] - ---[[About this plugin -This plugin adds the button(s) to the "Selected Images" module: -1) Return to Previous Collection -2) Collect on image's Folder -3) Collect on image's Color Label(s) -4) Collect on All (AND) - -It also adds 3 preferences to the lua options dialog box which allow the user to activate/deactivate the 3 "Collect on" buttons. - -Button Behavior: -1) Return to Previous Collection - Will reset the collect parameters to the previously active settings -2) Collect on image's Folder - Will change the collect parameters to be "Folder" with a value of the selected image's folder location -3) Collect on image's Color Label(s) - Will change the collect parameter to be "Color" with a value of the selected images color labels, will apply multiple parameters with AND logic if multiple exist -4) Collect on All (AND) - Will collect on all parameters activated by the preferences dialog, as such this button is redundant if you only have one of the two other options enabled - -----REQUIRED SOFTWARE---- -NA - -----USAGE---- -Install: (see here for more detail: https://github.com/darktable-org/lua-scripts ) - 1) Copy this file in to your "lua/contrib" folder where all other scripts reside. - 2) Require this file in your luarc file, as with any other dt plug-in - -Select the photo you wish to change you collection based on. -In the "Selected Images" module click on "Collect on this Image" - -----KNOWN ISSUES---- -]] - -local dt = require "darktable" -local du = require "lib/dtutils" -local gettext = dt.gettext -local previous = nil -local all_active = false - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("CollectHelper",dt.configuration.config_dir.."/lua/locale/") - -du.check_min_api_version("7.0.0", "CollectHelper") - --- return data structure for script_manager - -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, otherwise leave as nil -script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - -local function _(msgid) - return gettext.dgettext("CollectHelper", msgid) -end - --- FUNCTION -- -local function CheckSingleImage(selection) - if #selection ~= 1 then - dt.print(_("Please select a single image")) - return true - end - return false -end - -local function CheckHasColorLabel(selection) - local ret = false - for _,image in pairs(selection) do - if image.red then ret = true end - if image.blue then ret = true end - if image.green then ret = true end - if image.yellow then ret = true end - if image.purple then ret = true end - end - return ret -end -local function PreviousCollection() - if previous ~= nil then - previous = dt.gui.libs.collect.filter(previous) - end -end - -local function CollectOnFolder(all_rules, all_active) - local images = dt.gui.selection() - if CheckSingleImage(images) then - return - end - local rules = {} - local rule = dt.gui.libs.collect.new_rule() - rule.mode = "DT_LIB_COLLECT_MODE_AND" - rule.data = images[1].path - rule.item = "DT_COLLECTION_PROP_FOLDERS" - table.insert(rules, rule) - if all_active then - for _,active_rule in pairs(rules) do - table.insert(all_rules, active_rule) - end - return all_rules - else - previous = dt.gui.libs.collect.filter(rules) - end -end - -local function CollectOnColors(all_rules, all_active) - local images = dt.gui.selection() - if CheckSingleImage(images) then - return - end - if not CheckHasColorLabel(images) then - dt.print(_('select an image with an active color label')) - return - end - for _,image in pairs(images) do - local rules = {} - if image.red then - local red_rule = dt.gui.libs.collect.new_rule() - red_rule.mode = "DT_LIB_COLLECT_MODE_AND" - red_rule.data = "red" - red_rule.item = "DT_COLLECTION_PROP_COLORLABEL" - table.insert(rules, red_rule) - end - if image.blue then - local blue_rule = dt.gui.libs.collect.new_rule() - blue_rule.mode = "DT_LIB_COLLECT_MODE_AND" - blue_rule.data = "blue" - blue_rule.item = "DT_COLLECTION_PROP_COLORLABEL" - table.insert(rules, blue_rule) - end - if image.green then - local green_rule = dt.gui.libs.collect.new_rule() - green_rule.mode = "DT_LIB_COLLECT_MODE_AND" - green_rule.data = "green" - green_rule.item = "DT_COLLECTION_PROP_COLORLABEL" - table.insert(rules, green_rule) - end - if image.yellow then - local yellow_rule = dt.gui.libs.collect.new_rule() - yellow_rule.mode = "DT_LIB_COLLECT_MODE_AND" - yellow_rule.data = "yellow" - yellow_rule.item = "DT_COLLECTION_PROP_COLORLABEL" - table.insert(rules, yellow_rule) - end - if image.purple then - local purple_rule = dt.gui.libs.collect.new_rule() - purple_rule.mode = "DT_LIB_COLLECT_MODE_AND" - purple_rule.data = "purple" - purple_rule.item = "DT_COLLECTION_PROP_COLORLABEL" - table.insert(rules, purple_rule) - end - if all_active then - for _,active_rule in pairs(rules) do - table.insert(all_rules, active_rule) - end - return all_rules - else - previous = dt.gui.libs.collect.filter(rules) - end - end -end - -local function CollectOnAll_AND() - local images = dt.gui.selection() - if CheckSingleImage(images) then - return - end - local rules = {} - if dt.preferences.read('module_CollectHelper','folder','bool') then - rules = CollectOnFolder(rules, true) - end - if dt.preferences.read('module_CollectHelper','colors','bool') then - rules = CollectOnColors(rules, true) - end - previous = dt.gui.libs.collect.filter(rules) -end - -local function destroy() - dt.gui.libs.image.destroy_action("CollectHelper_prev") - if dt.preferences.read('module_CollectHelper','folder','bool') then - dt.gui.libs.image.destroy_action("CollectHelper_folder") - end - if dt.preferences.read('module_CollectHelper','colors','bool') then - dt.gui.libs.image.destroy_action("CollectHelper_labels") - end - if dt.preferences.read('module_CollectHelper','all_and','bool') then - dt.gui.libs.image.destroy_action("CollectHelper_and") - end -end - --- GUI -- -dt.gui.libs.image.register_action( - "CollectHelper_prev", _("collect: previous"), - function() PreviousCollection() end, - _("Sets the Collect parameters to be the previously active parameters") -) -if dt.preferences.read('module_CollectHelper','folder','bool') then - dt.gui.libs.image.register_action( - "CollectHelper_folder", _("collect: folder"), - function() CollectOnFolder(_ , false) end, - _("Sets the Collect parameters to be the selected images's folder") - ) -end -if dt.preferences.read('module_CollectHelper','colors','bool') then - dt.gui.libs.image.register_action( - "CollectHelper_labels", _("collect: color label(s)"), - function() CollectOnColors(_ , false) end, - _("Sets the Collect parameters to be the selected images's color label(s)") - ) -end -if dt.preferences.read('module_CollectHelper','all_and','bool') then - dt.gui.libs.image.register_action( - "CollectHelper_and", _("collect: all (AND)"), - function() CollectOnAll_AND() end, - _("Sets the Collect parameters based on all activated CollectHelper options") - ) -end - --- PREFERENCES -- -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 - true -- default -) -dt.preferences.register("module_CollectHelper", "colors", -- name - "bool", -- type - _('CollectHelper: Color Label(s)'), -- label - _('Enable the button that allows you to swap to a collection based on selected image\'s COLOR LABEL(S)'), -- tooltip - true -- default -) -dt.preferences.register("module_CollectHelper", "folder", -- name - "bool", -- type - _('CollectHelper: Folder'), -- label - _('Enable the button that allows you to swap to a collection based on selected image\'s FOLDER location'), -- tooltip - true -- default -) - -script_data.destroy = destroy - -return script_data +--[[Collect Helper plugin for darktable + + copyright (c) 2019 Kevin Ertel + + 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 . +]] + +--[[About this plugin +This plugin adds the button(s) to the "Selected Images" module: +1) Return to Previous Collection +2) Collect on image's Folder +3) Collect on image's Color Label(s) +4) Collect on All (AND) + +It also adds 3 preferences to the lua options dialog box which allow the user to activate/deactivate the 3 "Collect on" buttons. + +Button Behavior: +1) Return to Previous Collection - Will reset the collect parameters to the previously active settings +2) Collect on image's Folder - Will change the collect parameters to be "Folder" with a value of the selected image's folder location +3) Collect on image's Color Label(s) - Will change the collect parameter to be "Color" with a value of the selected images color labels, will apply multiple parameters with AND logic if multiple exist +4) Collect on All (AND) - Will collect on all parameters activated by the preferences dialog, as such this button is redundant if you only have one of the two other options enabled + +----REQUIRED SOFTWARE---- +NA + +----USAGE---- +Install: (see here for more detail: https://github.com/darktable-org/lua-scripts ) + 1) Copy this file in to your "lua/contrib" folder where all other scripts reside. + 2) Require this file in your luarc file, as with any other dt plug-in + +Select the photo you wish to change you collection based on. +In the "Selected Images" module click on "Collect on this Image" + +----KNOWN ISSUES---- +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local gettext = dt.gettext.gettext +local previous = nil +local all_active = false + +du.check_min_api_version("7.0.0", "CollectHelper") + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("collection helper"), + purpose = _("add collection helper buttons"), + author = "Kevin Ertel", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/CollectHelper/" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +-- FUNCTION -- +local function CheckSingleImage(selection) + if #selection ~= 1 then + dt.print(_("please select a single image")) + return true + end + return false +end + +local function CheckHasColorLabel(selection) + local ret = false + for _,image in pairs(selection) do + if image.red then ret = true end + if image.blue then ret = true end + if image.green then ret = true end + if image.yellow then ret = true end + if image.purple then ret = true end + end + return ret +end +local function PreviousCollection() + if previous ~= nil then + previous = dt.gui.libs.collect.filter(previous) + end +end + +local function CollectOnFolder(all_rules, all_active) + local images = dt.gui.selection() + if CheckSingleImage(images) then + return + end + local rules = {} + local rule = dt.gui.libs.collect.new_rule() + rule.mode = "DT_LIB_COLLECT_MODE_AND" + rule.data = images[1].path + rule.item = "DT_COLLECTION_PROP_FOLDERS" + table.insert(rules, rule) + if all_active then + for _,active_rule in pairs(rules) do + table.insert(all_rules, active_rule) + end + return all_rules + else + previous = dt.gui.libs.collect.filter(rules) + end +end + +local function CollectOnColors(all_rules, all_active) + local images = dt.gui.selection() + if CheckSingleImage(images) then + return + end + if not CheckHasColorLabel(images) then + dt.print(_('select an image with an active color label')) + return + end + for _,image in pairs(images) do + local rules = {} + if image.red then + local red_rule = dt.gui.libs.collect.new_rule() + red_rule.mode = "DT_LIB_COLLECT_MODE_AND" + red_rule.data = "red" + red_rule.item = "DT_COLLECTION_PROP_COLORLABEL" + table.insert(rules, red_rule) + end + if image.blue then + local blue_rule = dt.gui.libs.collect.new_rule() + blue_rule.mode = "DT_LIB_COLLECT_MODE_AND" + blue_rule.data = "blue" + blue_rule.item = "DT_COLLECTION_PROP_COLORLABEL" + table.insert(rules, blue_rule) + end + if image.green then + local green_rule = dt.gui.libs.collect.new_rule() + green_rule.mode = "DT_LIB_COLLECT_MODE_AND" + green_rule.data = "green" + green_rule.item = "DT_COLLECTION_PROP_COLORLABEL" + table.insert(rules, green_rule) + end + if image.yellow then + local yellow_rule = dt.gui.libs.collect.new_rule() + yellow_rule.mode = "DT_LIB_COLLECT_MODE_AND" + yellow_rule.data = "yellow" + yellow_rule.item = "DT_COLLECTION_PROP_COLORLABEL" + table.insert(rules, yellow_rule) + end + if image.purple then + local purple_rule = dt.gui.libs.collect.new_rule() + purple_rule.mode = "DT_LIB_COLLECT_MODE_AND" + purple_rule.data = "purple" + purple_rule.item = "DT_COLLECTION_PROP_COLORLABEL" + table.insert(rules, purple_rule) + end + if all_active then + for _,active_rule in pairs(rules) do + table.insert(all_rules, active_rule) + end + return all_rules + else + previous = dt.gui.libs.collect.filter(rules) + end + end +end + +local function CollectOnAll_AND() + local images = dt.gui.selection() + if CheckSingleImage(images) then + return + end + local rules = {} + if dt.preferences.read('module_CollectHelper','folder','bool') then + rules = CollectOnFolder(rules, true) + end + if dt.preferences.read('module_CollectHelper','colors','bool') then + rules = CollectOnColors(rules, true) + end + previous = dt.gui.libs.collect.filter(rules) +end + +local function destroy() + dt.gui.libs.image.destroy_action("CollectHelper_prev") + if dt.preferences.read('module_CollectHelper','folder','bool') then + dt.gui.libs.image.destroy_action("CollectHelper_folder") + end + if dt.preferences.read('module_CollectHelper','colors','bool') then + dt.gui.libs.image.destroy_action("CollectHelper_labels") + end + if dt.preferences.read('module_CollectHelper','all_and','bool') then + dt.gui.libs.image.destroy_action("CollectHelper_and") + end +end + +-- GUI -- +dt.gui.libs.image.register_action( + "CollectHelper_prev", _("collect: previous"), + function() PreviousCollection() end, + _("sets the collect parameters to be the previously active parameters") +) +if dt.preferences.read('module_CollectHelper','folder','bool') then + dt.gui.libs.image.register_action( + "CollectHelper_folder", _("collect: folder"), + function() CollectOnFolder(_ , false) end, + _("sets the collect parameters to be the selected images's folder") + ) +end +if dt.preferences.read('module_CollectHelper','colors','bool') then + dt.gui.libs.image.register_action( + "CollectHelper_labels", _("collect: color label(s)"), + function() CollectOnColors(_ , false) end, + _("sets the collect parameters to be the selected images's color label(s)") + ) +end +if dt.preferences.read('module_CollectHelper','all_and','bool') then + dt.gui.libs.image.register_action( + "CollectHelper_and", _("collect: all (AND)"), + function() CollectOnAll_AND() end, + _("sets the collect parameters based on all activated CollectHelper options") + ) +end + +-- PREFERENCES -- +dt.preferences.register("module_CollectHelper", "all_and", -- name + "bool", -- type + _('CollectHelper: all'), -- label + _('creates a collect parameter set that utilizes all enabled CollectHelper types (and)'), -- tooltip + true -- default +) +dt.preferences.register("module_CollectHelper", "colors", -- name + "bool", -- type + _('CollectHelper: color label(s)'), -- label + _('enable the button that allows you to swap to a collection based on selected image\'s color label(s)'), -- tooltip + true -- default +) +dt.preferences.register("module_CollectHelper", "folder", -- name + "bool", -- type + _('CollectHelper: folder'), -- label + _('enable the button that allows you to swap to a collection based on selected image\'s folder location'), -- tooltip + true -- default +) + +script_data.destroy = destroy + +return script_data diff --git a/contrib/HDRMerge.lua b/contrib/HDRMerge.lua index bf922989..9d7a951d 100644 --- a/contrib/HDRMerge.lua +++ b/contrib/HDRMerge.lua @@ -1,478 +1,486 @@ ---[[HDRMerge plugin for darktable - - copyright (c) 2018 Kevin Ertel - - 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 . -]] - ---[[About this Plugin -This plugin adds the module 'HDRMerge' to darktable's lighttable view - -----REQUIRED SOFTWARE---- -HDRMerge ver. 4.5 or greater - -----USAGE---- -Install: (see here for more detail: https://github.com/darktable-org/lua-scripts ) - 1) Copy this file in to your 'lua/contrib' folder where all other scripts reside. - 2) Require this file in your luarc file, as with any other dt plug-in -On the initial startup go to darktable settings > lua options and set your executable paths and other preferences, then restart darktable - -Select bracketed images and press the Run HDRMerge button. The resulting DNG will be auto-imported into darktable. -Additional tags or style can be applied on auto import as well, if you desire. - -Base Options: -Select your desired BPS (bits per sample and Embedded Preview Size. - -Batch Options: -Select if you want to run in batch mode or not -Select the gap, in seconds, between images for auto grouping in batch mode - -See HDRMerge manual for further detail: http://jcelaya.github.io/hdrmerge/documentation/2014/07/11/user-manual.html - -Auto-import Options: -Select a style, whether you want tags to be copied from the original, and any additional tags you desire added when the new image is auto-imported -]] - -local dt = require 'darktable' -local du = require "lib/dtutils" -local df = require 'lib/dtutils.file' -local dsys = require 'lib/dtutils.system' - -du.check_min_api_version("7.0.0", "HDRmerge") - --- return data structure for script_manager - -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, otherwise leave as nil -script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - - -local mod = 'module_HDRMerge' -local os_path_seperator = '/' -if dt.configuration.running_os == 'windows' then os_path_seperator = '\\' end -local CURR_API_STRING = dt.configuration.api_version_string - --- Tell gettext where to find the .mo file translating messages for a particular domain -local gettext = dt.gettext -gettext.bindtextdomain('HDRMerge', dt.configuration.config_dir..'/lua/locale/') -local function _(msgid) - return gettext.dgettext('HDRMerge', msgid) -end - -local temp -local HDRM = { --HDRMerge Program Table - name = 'HDRMerge', - bin = '', - first_run = true, - install_error = false, - arg_string = '', - images_string = '', - args = { - bps = {text = '-b ', style = 'integer'}, - size = {text = '-p ', style = 'string'}, - batch = {text = '-B ', style = 'bool'}, - gap = {text = '-g ', style = 'integer'} - } -} -local GUI = { --GUI Elements Table - HDR = { - bps ={}, - size ={}, - batch ={}, - gap ={} - }, - Target = { - style ={}, - copy_tags ={}, - add_tags ={} - }, - run = {}, - stack = {}, - options = {}, - exes = { - HDRMerge = {}, - update = {}, - } -} - -HDRM.module_installed = false -HDRM.event_registered = false - - ---Detect User Styles-- -local styles = dt.styles -local styles_count = 1 -- 'none' = 1 -for _,i in pairs(dt.styles) do - if type(i) == 'userdata' then styles_count = styles_count + 1 end -end - -local function InRange(test, low, high) --tests if test value is within range of low and high (inclusive) - if test >= low and test <= high then - return true - else - return false - end -end - -local function GetFileName(full_path) --Parses a full path (path/filename_identifier.extension) into individual parts ---[[Input: Folder1/Folder2/Folder3/Img_0001.CR2 - - Returns: - path: Folder1/Folder2/Folder3/ - filename: Img_0001 - identifier: 0001 - extension: .CR2 - - EX: - path_1, file_1, id_1, ext_1 = GetFileName(full_path_1) - ]] - local path = string.match(full_path, '.*[\\/]') - local filename = string.gsub(string.match(full_path, '[%w-_]*%.') , '%.' , '' ) - local identifier = string.match(filename, '%d*$') - local extension = string.match(full_path, '%.%w*') - return path, filename, identifier, extension -end - -local function CleanSpaces(text) --removes spaces from the front and back of passed in text - text = string.gsub(text,'^%s*','') - text = string.gsub(text,'%s*$','') - return text -end - -local function BuildExecuteCmd(prog_table) --creates a program command using elements of the passed in program table - local result = CleanSpaces(prog_table.bin)..' '..CleanSpaces(prog_table.arg_string)..' '..CleanSpaces(prog_table.images_string) - return result -end - -local function PreCall(prog_tbl) --looks to see if this is the first call, if so checks to see if program is installed properly - for _,prog in pairs(prog_tbl) do - if prog.first_run then - prog.bin = df.check_if_bin_exists(prog.name) - if not prog.bin then - prog.install_error = true - dt.preferences.write(mod, 'bin_exists', 'bool', false) - else - prog.bin = CleanSpaces(prog.bin) - end - prog.first_run = false - end - end - if not dt.preferences.read(mod, 'bin_exists', 'bool') then - GUI.stack.active = 2 - dt.print(_('please update you binary location')) - end -end - -local function ExeUpdate(prog_tbl) - dt.preferences.write(mod, 'bin_exists', 'bool', true) - for _,prog in pairs(prog_tbl) do - dt.preferences.write('executable_paths', prog.name, 'string', GUI.exes[prog.name].value) - prog.bin = df.check_if_bin_exists(prog.name) - if not prog.bin then - prog.install_error = true - dt.preferences.write(mod, 'bin_exists', 'bool', false) - else - prog.bin = CleanSpaces(prog.bin) - end - prog.first_run = false - end - if dt.preferences.read(mod, 'bin_exists', 'bool') then - GUI.stack.active = 1 - dt.print(_('update successful')) - else - dt.print(_('update unsuccessful, please try again')) - end -end - -local function UpdateActivePreference() --sliders & entry boxes do not have a click/changed callback, so their values must be saved to the active preference - temp = GUI.HDR.gap.value - dt.preferences.write(mod, 'active_gap', 'integer', temp) - temp = GUI.Target.add_tags.text - dt.preferences.write(mod, 'active_add_tags', 'string', temp) -end - -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')) - return - end - images = dt.gui.selection() --get selected images - if #images < 2 then --ensure enough images selected - dt.print(_('not enough images selected, select at least 2 images to merge')) - return - end - - UpdateActivePreference() --save current gui elements to active preference so those values will be pre-loaded at next startup - - --create image string and output path - HDRM.images_string = '' - local out_path = '' - local smallest_id = math.huge - local smallest_name = '' - local largest_id = 0 - local source_raw = {} - for _,image in pairs(images) do --loop to concat the images string, also track the image indexes for use in creating the final image name (eg; IMG_1034-1037.dng) - local curr_image = image.path..os_path_seperator..image.filename - HDRM.images_string = HDRM.images_string..df.sanitize_filename(curr_image)..' ' - out_path = image.path - _unused, source_name, source_id = GetFileName(image.filename) - source_id = tonumber(source_id) or 0 - if source_id < smallest_id then - smallest_id = source_id - smallest_name = source_name - source_raw = image - end - if source_id > largest_id then largest_id = source_id end - end - out_path = out_path..os_path_seperator..smallest_name..'-'..largest_id..'.dng' - out_path = df.create_unique_filename(out_path) - - --create argument string - HDRM.arg_string = HDRM.args.bps.text..GUI.HDR.bps.value..' '..HDRM.args.size.text..GUI.HDR.size.value..' ' - if GUI.HDR.batch.value then - HDRM.arg_string = HDRM.arg_string..HDRM.args.batch.text..HDRM.args.gap.text..math.floor(GUI.HDR.gap.value)..' -a' - else - HDRM.arg_string = HDRM.arg_string..'-o '..df.sanitize_filename(out_path) - end - - -- create run command and execute - local run_cmd = BuildExecuteCmd(HDRM) - resp = dsys.external_command(run_cmd) - - if resp == 0 and not GUI.HDR.batch.value then - local imported = dt.database.import(out_path) -- import the new file - if GUI.Target.style.selected > 1 then -- apply selected style - local set_style = styles[GUI.Target.style.selected - 1] - dt.styles.apply(set_style , imported) - end - if GUI.Target.copy_tags.value then -- copy tags from the original file (ignore 'darktable' generated tags) - local all_tags = dt.tags.get_tags(source_raw) - for _,tag in pairs(all_tags) do - if string.match(tag.name, 'darktable|') == nil then dt.tags.attach(tag, imported) end - end - end - local set_tag = GUI.Target.add_tags.text - if set_tag ~= nil then -- add additional user-specified tags - for tag in string.gmatch(set_tag, '[^,]+') do - tag = CleanSpaces(tag) - tag = dt.tags.create(tag) - dt.tags.attach(tag, imported) - end - end - dt.print(_('HDRMerge completed successfully')) - else - dt.print_error(_('HDRMerge failed')) - dt.print(_('HDRMerge failed')) - end - -end - -local function install_module() - if not HDRM.module_installed then - dt.register_lib( -- register HDRMerge module - 'HDRMerge_Lib', -- Module name - _('HDRMerge'), -- name - true, -- expandable - true, -- resetable - {[dt.gui.views.lighttable] = {'DT_UI_CONTAINER_PANEL_RIGHT_CENTER', 99}}, -- containers - dt.new_widget('box'){ - orientation = 'vertical', - GUI.stack - } - ) - HDRM.module_installed = true - end -end - -local function destroy() - dt.gui.libs["HDRMerge_Lib"].visible = false -end - -local function restart() - dt.gui.libs["HDRMerge_Lib"].visible = true -end - --- GUI Elements -- -local lbl_hdr = dt.new_widget('section_label'){ - label = _('HDRMerge options') -} -temp = dt.preferences.read(mod, 'active_bps_ind', 'integer') -if not InRange(temp, 1, 3) then temp = 3 end -GUI.HDR.bps = dt.new_widget('combobox'){ - label = _('bits per sample'), - tooltip =_('number of bits per sample in the output image'), - selected = temp, - '16','24','32', - changed_callback = function(self) - dt.preferences.write(mod, 'active_bps', 'integer', self.value) - dt.preferences.write(mod, 'active_bps_ind', 'integer', self.selected) - end, - reset_callback = function(self) - self.selected = 3 - dt.preferences.write(mod, 'active_bps', 'integer', self.value) - dt.preferences.write(mod, 'active_bps_ind', 'integer', self.selected) - end -} -temp = dt.preferences.read(mod, 'active_size_ind', 'integer') -if not InRange(temp, 1, 3) then temp = 2 end -GUI.HDR.size = dt.new_widget('combobox'){ - label = _('embedded preview size'), - tooltip =_('size of the embedded preview in output image'), - selected = temp, - _('none'),_('half'),_('full'), - changed_callback = function(self) - dt.preferences.write(mod, 'active_size', 'string', self.value) - dt.preferences.write(mod, 'active_size_ind', 'integer', self.selected) - end, - reset_callback = function(self) - self.selected = 2 - dt.preferences.write(mod, 'active_size', 'string', self.value) - dt.preferences.write(mod, 'active_size_ind', 'integer', self.selected) - end -} -GUI.HDR.batch = dt.new_widget('check_button'){ - label = _('batch mode'), - value = dt.preferences.read(mod, 'active_batch', 'bool'), - tooltip = _('enable batch mode operation \nNOTE: resultant files will NOT be auto-imported'), - clicked_callback = function(self) - dt.preferences.write(mod, 'active_batch', 'bool', self.value) - GUI.HDR.gap.sensitive = self.value - end, - reset_callback = function(self) self.value = false end -} -temp = dt.preferences.read(mod, 'active_gap', 'integer') -if not InRange(temp, 1, 3600) then temp = 3 end -GUI.HDR.gap = dt.new_widget('slider'){ - label = _('batch gap [sec.]'), - tooltip = _('gap, in seconds, between batch mode groups'), - soft_min = 1, - soft_max = 30, - hard_min = 1, - hard_max = 3600, - step = 1, - digits = 0, - value = temp, - sensitive = GUI.HDR.batch.value, - reset_callback = function(self) - self.value = 3 - end -} -local lbl_import = dt.new_widget('section_label'){ - label = _('import options') -} -GUI.Target.style = dt.new_widget('combobox'){ - label = _('apply style on import'), - tooltip = _('Apply selected style on auto-import to newly created image'), - selected = 1, - _('none'), - changed_callback = function(self) - dt.preferences.write(mod, 'active_style', 'string', self.value) - dt.preferences.write(mod, 'active_style_ind', 'integer', self.selected) - end, - reset_callback = function(self) - self.selected = 1 - dt.preferences.write(mod, 'active_style', 'string', self.value) - dt.preferences.write(mod, 'active_style_ind', 'integer', self.selected) - end -} -for k=1, (styles_count-1) do - GUI.Target.style[k+1] = styles[k].name -end -temp = dt.preferences.read(mod, 'active_style_ind', 'integer') -if not InRange(temp, 1, styles_count) then temp = 1 end -GUI.Target.style.selected = temp -GUI.Target.copy_tags = dt.new_widget('check_button'){ - label = _('copy tags'), - value = dt.preferences.read(mod, 'active_copy_tags', 'bool'), - tooltip = _('copy tags from first source image'), - clicked_callback = function(self) dt.preferences.write(mod, 'active_copy_tags', 'bool', self.value) end, - reset_callback = function(self) self.value = true end -} -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'), - text = temp, - placeholder = _('Enter tags, seperated by commas'), - editable = true -} -GUI.run = dt.new_widget('button'){ - label = _('merge'), - tooltip =_('run HDRMerge with the above specified settings'), - clicked_callback = function() main() end -} -GUI.exes.HDRMerge = dt.new_widget('file_chooser_button'){ - title = _('Select HDRmerge executable'), - value = df.get_executable_path_preference(HDRM.name), - is_directory = false -} -GUI.exes.update = dt.new_widget('button'){ - label = _('update'), - tooltip =_('update the binary path with current value'), - clicked_callback = function() ExeUpdate({HDRM}) end -} -GUI.options = dt.new_widget('box'){ - orientation = 'vertical', - lbl_hdr, - GUI.HDR.bps, - GUI.HDR.size, - GUI.HDR.batch, - GUI.HDR.gap, - lbl_import, - GUI.Target.style, - GUI.Target.copy_tags, - GUI.Target.add_tags, - GUI.run -} -local exes_box = dt.new_widget('box'){ - orientation = 'vertical', - GUI.exes.HDRMerge, - GUI.exes.update -} -GUI.stack = dt.new_widget('stack'){ - GUI.options, - exes_box -} -if dt.preferences.read(mod, 'bin_exists', 'bool') then - GUI.stack.active = 1 -else - GUI.stack.active = 2 -end - -if dt.gui.current_view().id == "lighttable" then - install_module() -else - if not HDRM.event_registered then - dt.register_event( - "HDRmerge", "view-changed", - function(event, old_view, new_view) - if new_view.name == "lighttable" and old_view.name == "darkroom" then - install_module() - end - end - ) - HDRM.event_registered = true - end -end - -script_data.destroy = destroy -script_data.restart = restart -script_data.destroy_method = "hide" -script_data.show = restart - +--[[HDRMerge plugin for darktable + + copyright (c) 2018 Kevin Ertel + + 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 . +]] + +--[[About this Plugin +This plugin adds the module 'HDRMerge' to darktable's lighttable view + +----REQUIRED SOFTWARE---- +HDRMerge ver. 4.5 or greater + +----USAGE---- +Install: (see here for more detail: https://github.com/darktable-org/lua-scripts ) + 1) Copy this file in to your 'lua/contrib' folder where all other scripts reside. + 2) Require this file in your luarc file, as with any other dt plug-in +On the initial startup go to darktable settings > lua options and set your executable paths and other preferences, then restart darktable + +Select bracketed images and press the Run HDRMerge button. The resulting DNG will be auto-imported into darktable. +Additional tags or style can be applied on auto import as well, if you desire. + +Base Options: +Select your desired BPS (bits per sample and Embedded Preview Size. + +Batch Options: +Select if you want to run in batch mode or not +Select the gap, in seconds, between images for auto grouping in batch mode + +See HDRMerge manual for further detail: http://jcelaya.github.io/hdrmerge/documentation/2014/07/11/user-manual.html + +Auto-import Options: +Select a style, whether you want tags to be copied from the original, and any additional tags you desire added when the new image is auto-imported +]] + +local dt = require 'darktable' +local du = require "lib/dtutils" +local df = require 'lib/dtutils.file' +local dsys = require 'lib/dtutils.system' + +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 + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + 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" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + + +local mod = 'module_HDRMerge' +local os_path_seperator = '/' +if dt.configuration.running_os == 'windows' then os_path_seperator = '\\' end +local CURR_API_STRING = dt.configuration.api_version_string + +local temp +local HDRM = { --HDRMerge Program Table + name = 'HDRMerge', + bin = '', + first_run = true, + install_error = false, + arg_string = '', + images_string = '', + args = { + bps = {text = '-b ', style = 'integer'}, + size = {text = '-p ', style = 'string'}, + batch = {text = '-B ', style = 'bool'}, + gap = {text = '-g ', style = 'integer'} + } +} +local GUI = { --GUI Elements Table + HDR = { + bps ={}, + size ={}, + batch ={}, + gap ={} + }, + Target = { + style ={}, + copy_tags ={}, + add_tags ={} + }, + run = {}, + stack = {}, + options = {}, + exes = { + HDRMerge = {}, + update = {}, + } +} + +HDRM.module_installed = false +HDRM.event_registered = false + + +--Detect User Styles-- +local styles = dt.styles +local styles_count = 1 -- 'none' = 1 +for _,i in pairs(dt.styles) do + if type(i) == 'userdata' then styles_count = styles_count + 1 end +end + +local function InRange(test, low, high) --tests if test value is within range of low and high (inclusive) + if test >= low and test <= high then + return true + else + return false + end +end + +local function GetFileName(full_path) --Parses a full path (path/filename_identifier.extension) into individual parts +--[[Input: Folder1/Folder2/Folder3/Img_0001.CR2 + + Returns: + path: Folder1/Folder2/Folder3/ + filename: Img_0001 + identifier: 0001 + extension: .CR2 + + EX: + path_1, file_1, id_1, ext_1 = GetFileName(full_path_1) + ]] + local path = string.match(full_path, '.*[\\/]') + local filename = string.gsub(string.match(full_path, '[%w-_]*%.') , '%.' , '' ) + local identifier = string.match(filename, '%d*$') + local extension = string.match(full_path, '%.%w*') + return path, filename, identifier, extension +end + +local function CleanSpaces(text) --removes spaces from the front and back of passed in text + text = string.gsub(text,'^%s*','') + text = string.gsub(text,'%s*$','') + return text +end + +local function BuildExecuteCmd(prog_table) --creates a program command using elements of the passed in program table + local result = CleanSpaces(prog_table.bin)..' '..CleanSpaces(prog_table.arg_string)..' '..CleanSpaces(prog_table.images_string) + return result +end + +local function PreCall(prog_tbl) --looks to see if this is the first call, if so checks to see if program is installed properly + for _,prog in pairs(prog_tbl) do + if prog.first_run then + prog.bin = df.check_if_bin_exists(prog.name) + if not prog.bin then + prog.install_error = true + dt.preferences.write(mod, 'bin_exists', 'bool', false) + else + prog.bin = CleanSpaces(prog.bin) + end + prog.first_run = false + end + end + if not dt.preferences.read(mod, 'bin_exists', 'bool') then + GUI.stack.active = 2 + dt.print(_('please update you binary location')) + end +end + +local function ExeUpdate(prog_tbl) + dt.preferences.write(mod, 'bin_exists', 'bool', true) + for _,prog in pairs(prog_tbl) do + dt.preferences.write('executable_paths', prog.name, 'string', GUI.exes[prog.name].value) + prog.bin = df.check_if_bin_exists(prog.name) + if not prog.bin then + prog.install_error = true + dt.preferences.write(mod, 'bin_exists', 'bool', false) + else + prog.bin = CleanSpaces(prog.bin) + end + prog.first_run = false + end + if dt.preferences.read(mod, 'bin_exists', 'bool') then + GUI.stack.active = 1 + dt.print(_('update successful')) + else + dt.print(_('update unsuccessful, please try again')) + end +end + +local function UpdateActivePreference() --sliders & entry boxes do not have a click/changed callback, so their values must be saved to the active preference + temp = GUI.HDR.gap.value + dt.preferences.write(mod, 'active_gap', 'integer', temp) + temp = GUI.Target.add_tags.text + dt.preferences.write(mod, 'active_add_tags', 'string', temp) +end + +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 correct')) + return + end + images = dt.gui.selection() --get selected images + if #images < 2 then --ensure enough images selected + dt.print(_('not enough images selected, select at least 2 images to merge')) + return + end + + UpdateActivePreference() --save current gui elements to active preference so those values will be pre-loaded at next startup + + --create image string and output path + HDRM.images_string = '' + local out_path = '' + local smallest_id = math.huge + local smallest_name = '' + local largest_id = 0 + local source_raw = {} + for _,image in pairs(images) do --loop to concat the images string, also track the image indexes for use in creating the final image name (eg; IMG_1034-1037.dng) + local curr_image = image.path..os_path_seperator..image.filename + HDRM.images_string = HDRM.images_string..df.sanitize_filename(curr_image)..' ' + out_path = image.path + _unused, source_name, source_id = GetFileName(image.filename) + source_id = tonumber(source_id) or 0 + if source_id < smallest_id then + smallest_id = source_id + smallest_name = source_name + source_raw = image + end + if source_id > largest_id then largest_id = source_id end + end + out_path = out_path..os_path_seperator..smallest_name..'-'..largest_id..'.dng' + out_path = df.create_unique_filename(out_path) + + --create argument string + HDRM.arg_string = HDRM.args.bps.text..GUI.HDR.bps.value..' '..HDRM.args.size.text..GUI.HDR.size.value..' ' + if GUI.HDR.batch.value then + HDRM.arg_string = HDRM.arg_string..HDRM.args.batch.text..HDRM.args.gap.text..math.floor(GUI.HDR.gap.value)..' -a' + else + HDRM.arg_string = HDRM.arg_string..'-o '..df.sanitize_filename(out_path) + end + + -- create run command and execute + local run_cmd = BuildExecuteCmd(HDRM) + resp = dsys.external_command(run_cmd) + + if resp == 0 and not GUI.HDR.batch.value then + local imported = dt.database.import(out_path) -- import the new file + if GUI.Target.style.selected > 1 then -- apply selected style + local set_style = styles[GUI.Target.style.selected - 1] + dt.styles.apply(set_style , imported) + end + if GUI.Target.copy_tags.value then -- copy tags from the original file (ignore 'darktable' generated tags) + local all_tags = dt.tags.get_tags(source_raw) + for _,tag in pairs(all_tags) do + if string.match(tag.name, 'darktable|') == nil then dt.tags.attach(tag, imported) end + end + end + local set_tag = GUI.Target.add_tags.text + if set_tag ~= nil then -- add additional user-specified tags + for tag in string.gmatch(set_tag, '[^,]+') do + tag = CleanSpaces(tag) + tag = dt.tags.create(tag) + dt.tags.attach(tag, imported) + end + end + dt.print(_('HDRMerge completed successfully')) + else + dt.print_error('HDRMerge failed') + dt.print(_('HDRMerge failed')) + end + +end + +local function install_module() + if not HDRM.module_installed then + dt.register_lib( -- register HDRMerge module + 'HDRMerge_Lib', -- Module name + _('HDRMerge'), -- name + true, -- expandable + true, -- resetable + {[dt.gui.views.lighttable] = {'DT_UI_CONTAINER_PANEL_RIGHT_CENTER', 99}}, -- containers + dt.new_widget('box'){ + orientation = 'vertical', + GUI.stack + } + ) + HDRM.module_installed = true + end +end + +local function destroy() + dt.gui.libs["HDRMerge_Lib"].visible = false +end + +local function restart() + dt.gui.libs["HDRMerge_Lib"].visible = true +end + +-- GUI Elements -- +local lbl_hdr = dt.new_widget('section_label'){ + label = _('HDRMerge options') +} +temp = dt.preferences.read(mod, 'active_bps_ind', 'integer') +if not InRange(temp, 1, 3) then temp = 3 end +GUI.HDR.bps = dt.new_widget('combobox'){ + label = _('bits per sample'), + tooltip =_('number of bits per sample in the output image'), + selected = temp, + '16','24','32', + changed_callback = function(self) + dt.preferences.write(mod, 'active_bps', 'integer', self.value) + dt.preferences.write(mod, 'active_bps_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 3 + dt.preferences.write(mod, 'active_bps', 'integer', self.value) + dt.preferences.write(mod, 'active_bps_ind', 'integer', self.selected) + end +} +temp = dt.preferences.read(mod, 'active_size_ind', 'integer') +if not InRange(temp, 1, 3) then temp = 2 end +GUI.HDR.size = dt.new_widget('combobox'){ + label = _('embedded preview size'), + tooltip =_('size of the embedded preview in output image'), + selected = temp, + _('none'),_('half'),_('full'), + changed_callback = function(self) + dt.preferences.write(mod, 'active_size', 'string', self.value) + dt.preferences.write(mod, 'active_size_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 2 + dt.preferences.write(mod, 'active_size', 'string', self.value) + dt.preferences.write(mod, 'active_size_ind', 'integer', self.selected) + end +} +GUI.HDR.batch = dt.new_widget('check_button'){ + label = _('batch mode'), + value = dt.preferences.read(mod, 'active_batch', 'bool'), + tooltip = _('enable batch mode operation \nNOTE: resultant files will NOT be auto-imported'), + clicked_callback = function(self) + dt.preferences.write(mod, 'active_batch', 'bool', self.value) + GUI.HDR.gap.sensitive = self.value + end, + reset_callback = function(self) self.value = false end +} +temp = dt.preferences.read(mod, 'active_gap', 'integer') +if not InRange(temp, 1, 3600) then temp = 3 end +GUI.HDR.gap = dt.new_widget('slider'){ + label = _('batch gap [sec.]'), + tooltip = _('gap, in seconds, between batch mode groups'), + soft_min = 1, + soft_max = 30, + hard_min = 1, + hard_max = 3600, + step = 1, + digits = 0, + value = temp, + sensitive = GUI.HDR.batch.value, + reset_callback = function(self) + self.value = 3 + end +} +local lbl_import = dt.new_widget('section_label'){ + label = _('import options') +} +GUI.Target.style = dt.new_widget('combobox'){ + label = _('apply style on import'), + tooltip = _('apply selected style on auto-import to newly created image'), + selected = 1, + _('none'), + changed_callback = function(self) + dt.preferences.write(mod, 'active_style', 'string', self.value) + dt.preferences.write(mod, 'active_style_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 1 + dt.preferences.write(mod, 'active_style', 'string', self.value) + dt.preferences.write(mod, 'active_style_ind', 'integer', self.selected) + end +} +for k=1, (styles_count-1) do + GUI.Target.style[k+1] = styles[k].name +end +temp = dt.preferences.read(mod, 'active_style_ind', 'integer') +if not InRange(temp, 1, styles_count) then temp = 1 end +GUI.Target.style.selected = temp +GUI.Target.copy_tags = dt.new_widget('check_button'){ + label = _('copy tags'), + value = dt.preferences.read(mod, 'active_copy_tags', 'bool'), + tooltip = _('copy tags from first source image'), + clicked_callback = function(self) dt.preferences.write(mod, 'active_copy_tags', 'bool', self.value) end, + reset_callback = function(self) self.value = true end +} +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, separate with commas, all spaces will be removed'), + text = temp, + placeholder = _('enter tags, separated by commas'), + editable = true +} +GUI.run = dt.new_widget('button'){ + label = _('merge'), + tooltip =_('run HDRMerge with the above settings'), + clicked_callback = function() main() end +} +GUI.exes.HDRMerge = dt.new_widget('file_chooser_button'){ + title = _('select HDRmerge executable'), + value = df.get_executable_path_preference(HDRM.name), + is_directory = false +} +GUI.exes.update = dt.new_widget('button'){ + label = _('update'), + tooltip =_('update the binary path with current value'), + clicked_callback = function() ExeUpdate({HDRM}) end +} +GUI.options = dt.new_widget('box'){ + orientation = 'vertical', + lbl_hdr, + GUI.HDR.bps, + GUI.HDR.size, + GUI.HDR.batch, + GUI.HDR.gap, + lbl_import, + GUI.Target.style, + GUI.Target.copy_tags, + GUI.Target.add_tags, + GUI.run +} +local exes_box = dt.new_widget('box'){ + orientation = 'vertical', + GUI.exes.HDRMerge, + GUI.exes.update +} +GUI.stack = dt.new_widget('stack'){ + GUI.options, + exes_box +} +if dt.preferences.read(mod, 'bin_exists', 'bool') then + GUI.stack.active = 1 +else + GUI.stack.active = 2 +end + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not HDRM.event_registered then + dt.register_event( + "HDRmerge", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + HDRM.event_registered = true + end +end + +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = restart + return script_data \ No newline at end of file diff --git a/contrib/LabelsToTags.lua b/contrib/LabelsToTags.lua index f5d87ba2..32031a3b 100644 --- a/contrib/LabelsToTags.lua +++ b/contrib/LabelsToTags.lua @@ -52,13 +52,27 @@ local du = require "lib/dtutils" du.check_min_api_version("7.0.0", "LabelsToTags") +local gettext = darktable.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them -- Lua 5.3 no longer has "unpack" but "table.unpack" unpack = unpack or table.unpack @@ -67,7 +81,7 @@ local ltt = {} ltt.module_installed = false ltt.event_registered = false -local LIB_ID = "LabelsToTags" +local LIB_ID = _("LabelsToTags") -- Helper functions: BEGIN @@ -108,23 +122,23 @@ end local initialAvailableMappings = { - ["Colors"] = { ["+*****"] = { "Red" }, - ["*+****"] = { "Yellow" }, - ["**+***"] = { "Green" }, - ["***+**"] = { "Blue" }, - ["****+*"] = { "Purple" } }, - ["Single colors"] = { ["+----*"] = { "Red", "Only red" }, - ["-+---*"] = { "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" }, - ["*****3"] = { "Three stars", "Not rejected" }, - ["*****4"] = { "Four stars", "Not rejected" }, - ["*****5"] = { "Five stars", "Not rejected" }, - ["*****R"] = { "Rejected" } } + [_("colors")] = { ["+*****"] = { _("red") }, + ["*+****"] = { _("yellow") }, + ["**+***"] = { _("green") }, + ["***+**"] = { _("blue") }, + ["****+*"] = { _("purple") } }, + [_("single colors")] = { ["+----*"] = { _("red"), _("only red") }, + ["-+---*"] = { _("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") }, + ["*****3"] = { _("three stars"), _("not rejected") }, + ["*****4"] = { _("four stars"), _("not rejected") }, + ["*****5"] = { _("five stars"), _("not rejected") }, + ["*****R"] = { _("rejected") } } } local availableMappings = {} @@ -139,14 +153,14 @@ end local function getComboboxTooltip() if availableMappings == nil or next(availableMappings) == nil then - return("No registered mappings -- using defaults") + return(_("no registered mappings -- using defaults")) else - return("Select a label-to-tag mapping") + return(_("select a label-to-tag mapping")) end end local mappingComboBox = darktable.new_widget("combobox"){ - label = "mapping", + label = _("mapping"), value = 1, tooltip = getComboboxTooltip(), reset_callback = function(selfC) @@ -169,7 +183,7 @@ local mappingComboBox = darktable.new_widget("combobox"){ } local function doTagging(selfC) - local job = darktable.gui.create_job(string.format("labels to tags (%d image" .. (#(darktable.gui.action_images) == 1 and "" or "s") .. ")",#(darktable.gui.action_images)),true) + local job = darktable.gui.create_job(string.format(_("labels to tags (%d image%s)"), #(darktable.gui.action_images), (#(darktable.gui.action_images) == 1 and "" or "s")), true) job.percent = 0.0 local pctIncrement = 1.0 / #(darktable.gui.action_images) @@ -200,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 } } @@ -231,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 a7761989..14f788d7 100644 --- a/contrib/OpenInExplorer.lua +++ b/contrib/OpenInExplorer.lua @@ -1,237 +1,261 @@ ---[[ -OpenInExplorer plugin for darktable - - copyright (c) 2018 Kevin Ertel - Update 2020 and macOS support by Volker Lenhardt - Linux support 2020 by 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 - 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 . -]] - ---[[About this plugin -This plugin adds the module "OpenInExplorer" to darktable's lighttable view. - -----REQUIRED SOFTWARE---- -Apple macOS, Microsoft Windows or Linux - -----USAGE---- -Install: (see here for more detail: https://github.com/darktable-org/lua-scripts ) - 1) Copy this file into your "lua/contrib" folder where all other scripts reside. - 2) Require this file in your luarc file, as with any other dt plug-in - -Select the photo(s) you wish to find in your operating system's file manager and press "show in file explorer" in the "selected images" section. - -- Nautilus (Linux), Explorer (Windows), and Finder (macOS prior to Mojave) will open one window for each selected image at the file's location. The file name will be highlighted. - -- On macOS Mojave and Catalina the Finder will open one window for each different directory. In these windows only the last one of the corresponding files will be highlighted (bug or feature?). - -- Dolphin (Linux) will open one window with tabs for the different directories. All the selected images' file names are highlighted in their respective directories. - -As an alternative option you can choose to show the image file names as symbolic links in an arbitrary directory. Go to preferences|Lua options. This option is not available for Windows users as on Windows solely admins are allowed to create links. - -- Pros: You do not clutter up your display with multiple windows. So there is no need to limit the number of selections. - -- Cons: If you want to work with the files you are one step behind the original data. - -----KNOWN ISSUES---- -]] - -local dt = require "darktable" -local du = require "lib/dtutils" -local df = require "lib/dtutils.file" -local dsys = require "lib/dtutils.system" -local gettext = dt.gettext - ---Check API version -du.check_min_api_version("7.0.0", "OpenInExplorer") - --- return data structure for script_manager - -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, otherwise leave as nil -script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("OpenInExplorer",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("OpenInExplorer", msgid) -end - -local act_os = dt.configuration.running_os -local PS = act_os == "windows" and "\\" or "/" - ---Detect OS and quit if it is not supported. -if act_os ~= "macos" and act_os ~= "windows" and act_os ~= "linux" then - dt.print(_("OpenInExplorer plug-in only supports Linux, macOS, and Windows at this time")) - dt.print_error("OpenInExplorer plug-in only supports Linux, macOS, and Windows at this time") - return -end - -local use_links, links_dir = false, "" -if act_os ~= "windows" then - use_links = dt.preferences.read("OpenInExplorer", "use_links", "bool") - links_dir = dt.preferences.read("OpenInExplorer", "linked_image_files_dir", "string") -end - ---Check if the directory exists that was chosen for the file links. Return boolean. -local function check_if_links_dir_exists() - local dir_exists = true - if not links_dir then - --Just for paranoic reasons. I tried, but I couldn't devise a setting for a nil value. - dt.print(_("No links directory selected.\nPlease check the dt preferences (lua options)")) - dt.print_error("OpenInExplorer: No links directory selected") - dir_exists = false - elseif not df.check_if_file_exists(links_dir) then - dt.print(string.format(_("Links directory '%s' not found.\nPlease check the dt preferences (lua options)"), links_dir)) - dt.print_error(string.format("OpenInExplorer: Links directory '%s' not found", links_dir)) - dir_exists = false - end - return dir_exists -end - ---Format strings for the commands to open the corresponding OS's file manager. -local open_dir = {} -open_dir.windows = "explorer.exe /n, %s" -open_dir.macos = "open %s" -open_dir.linux = [[busctl --user call org.freedesktop.FileManager1 /org/freedesktop/FileManager1 org.freedesktop.FileManager1 ShowFolders ass 1 %s ""]] - -local open_files = {} -open_files.windows = "explorer.exe /select, %s" -open_files.macos = "open -Rn %s" -open_files.linux = [[busctl --user call org.freedesktop.FileManager1 /org/freedesktop/FileManager1 org.freedesktop.FileManager1 ShowItems ass %d %s ""]] - ---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) - local current_image, file_uris, run_cmd = "", "", "" - for _, image in pairs(selected_images) do - current_image = image.path..PS..image.filename - file_uris = file_uris .. df.sanitize_filename("file://" .. current_image) .. " " - dt.print_log("file_uris is " .. file_uris) - end - run_cmd = string.format(open_files.linux, #selected_images, file_uris) - dt.print_log("OpenInExplorer run_cmd = "..run_cmd) - dsys.external_command(run_cmd) -end - ---Call the file manager for each selected image on Windows and macOS. -local function call_file_by_file(selected_images) - local current_image, run_cmd = "", "" - for _, image in pairs(selected_images) do - current_image = image.path..PS..image.filename - run_cmd = string.format(open_files[act_os], df.sanitize_filename(current_image)) - dt.print_log("OpenInExplorer run_cmd = "..run_cmd) - dsys.external_command(run_cmd) - end -end - ---Create a link for each selected image, and finally call the file manager. -local function set_links(selected_images) - local current_image, link_target, run_cmd, k = "", "", "", nil - for k, image in pairs(selected_images) do - current_image = image.path..PS..image.filename - link_target = df.create_unique_filename(links_dir .. PS .. image.filename) - run_cmd = string.format("ln -s %s %s", df.sanitize_filename(current_image), df.sanitize_filename(link_target)) - --[[ - In case Windows will allow normal users to create soft links: - if act_os == "windows" then - run_cmd = string.format("mklink %s %s", df.sanitize_filename(link_target), df.sanitize_filename(current_image)) - end - ]] - if dsys.external_command(run_cmd) ~= 0 then - dt.print(_("Failed to create links. Missing rights?")) - dt.print_error("OpenInExplorer: Failed to create links") - return - end - end - --The URI format is necessary only for the Linux busctl command. - --But it is accepted by the Windows Explorer and macOS's Finder all the same. - run_cmd = string.format(open_dir[act_os], df.sanitize_filename("file://"..links_dir)) - dt.print_log("OpenInExplorer run_cmd = "..run_cmd) - dsys.external_command(run_cmd) -end - ---The working function that starts the particular task. -local function open_in_fmanager() - local images = dt.gui.selection() - if #images == 0 then - dt.print(_("Please select an image")) - else - if use_links and not check_if_links_dir_exists() then - return - end - if #images > 15 and not use_links then - dt.print(_("Please select fewer images (max. 15)")) - elseif use_links then - set_links(images) - else - if act_os == "linux" then - call_list_of_files(images) - else - call_file_by_file(images) - end - end - end -end - -local function destroy() - dt.gui.libs.image.destroy_action("OpenInExplorer") - dt.destroy_event("OpenInExplorer", "shortcut") - if act_os ~= "windows" then - dt.preferences.destroy("OpenInExplorer", "linked_image_files_dir") - end - dt.preferences.destroy("OpenInExplorer", "use_links") -end - - --- GUI -- -dt.gui.libs.image.register_action( - "OpenInExplorer", _("show in file explorer"), - function() open_in_fmanager() end, - _("Open the file manager at the selected image's location") -) - - -if act_os ~= "windows" then - dt.preferences.register("OpenInExplorer", "linked_image_files_dir", -- name - "directory", -- type - _("OpenInExplorer: linked files directory"), -- label - _("Directory to store the links to the file names. Requires restart to take effect"), -- tooltip - "Links to image files", -- default - dt.new_widget("file_chooser_button"){ - title = _("Select directory"), - is_directory = true, - } - ) - dt.preferences.register("OpenInExplorer", "use_links", -- name - "bool", -- type - _("OpenInExplorer: use links"), -- label - _("Use links instead of multiple windows. Requires restart to take effect"), -- tooltip - false, -- default - "" - ) -end - -dt.register_event( - "OpenInExplorer", "shortcut", - function(event, shortcut) open_in_fmanager() end, - "OpenInExplorer" -) - -script_data.destroy = destroy - -return script_data +--[[ +OpenInExplorer plugin for darktable + + copyright (c) 2018 Kevin Ertel + Update 2020 and macOS support by Volker Lenhardt + Linux support 2020 by 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 + 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 . +]] + +--[[About this plugin +This plugin adds the module "OpenInExplorer" to darktable's lighttable view. + +----REQUIRED SOFTWARE---- +Apple macOS, Microsoft Windows or Linux + +----USAGE---- +Install: (see here for more detail: https://github.com/darktable-org/lua-scripts ) + 1) Copy this file into your "lua/contrib" folder where all other scripts reside. + 2) Require this file in your luarc file, as with any other dt plug-in + +Select the photo(s) you wish to find in your operating system's file manager and press "show in file explorer" in the "selected images" section. + +- Nautilus (Linux), Explorer (Windows), and Finder (macOS prior to Mojave) will open one window for each selected image at the file's location. The file name will be highlighted. + +- On macOS Mojave and Catalina the Finder will open one window for each different directory. In these windows only the last one of the corresponding files will be highlighted (bug or feature?). + +- Dolphin (Linux) will open one window with tabs for the different directories. All the selected images' file names are highlighted in their respective directories. + +As an alternative option you can choose to show the image file names as symbolic links in an arbitrary directory. Go to preferences|Lua options. This option is not available for Windows users as on Windows solely admins are allowed to create links. + +- Pros: You do not clutter up your display with multiple windows. So there is no need to limit the number of selections. + +- Cons: If you want to work with the files you are one step behind the original data. + +----KNOWN ISSUES---- +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local dsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext + +--Check API version +du.check_min_api_version("7.0.0", "OpenInExplorer") + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + 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" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +local act_os = dt.configuration.running_os +local PS = act_os == "windows" and "\\" or "/" + +--Detect OS and quit if it is not supported. +if act_os ~= "macos" and act_os ~= "windows" and act_os ~= "linux" then + dt.print(_("OpenInExplorer plug-in only supports linux, macos, and windows at this time")) + dt.print_error("OpenInExplorer plug-in only supports Linux, macOS, and Windows at this time") + return +end + +local use_links, links_dir = false, "" +if act_os ~= "windows" then + use_links = dt.preferences.read("OpenInExplorer", "use_links", "bool") + links_dir = dt.preferences.read("OpenInExplorer", "linked_image_files_dir", "string") +end + +--Check if the directory exists that was chosen for the file links. Return boolean. +local function check_if_links_dir_exists() + local dir_exists = true + if not links_dir then + --Just for paranoic reasons. I tried, but I couldn't devise a setting for a nil value. + dt.print(_("no links directory selected\nplease check the dt preferences (lua options)")) + dt.print_error("OpenInExplorer: No links directory selected") + dir_exists = false + elseif not df.check_if_file_exists(links_dir) then + dt.print(string.format(_("links directory '%s' not found\nplease check the dt preferences (lua options)"), links_dir)) + dt.print_error(string.format("OpenInExplorer: Links directory '%s' not found", links_dir)) + dir_exists = false + end + return dir_exists +end + +--Format strings for the commands to open the corresponding OS's file manager. +local open_dir = {} +open_dir.windows = "explorer.exe /n, %s" +open_dir.macos = "open %s" +open_dir.linux = [[busctl --user call org.freedesktop.FileManager1 /org/freedesktop/FileManager1 org.freedesktop.FileManager1 ShowFolders ass 1 %s ""]] + +local open_files = {} +open_files.windows = "explorer.exe /select, %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) + local current_image, file_uris, run_cmd = "", "", "" + for _, image in pairs(selected_images) do + current_image = image.path..PS..image.filename + file_uris = file_uris .. df.sanitize_filename("file://" .. current_image) .. " " + dt.print_log("file_uris is " .. file_uris) + end + run_cmd = string.format(open_files.linux, #selected_images, file_uris) + dt.print_log("OpenInExplorer run_cmd = "..run_cmd) + dsys.external_command(run_cmd) +end + +--Call the file manager for each selected image on Windows and macOS. +local function call_file_by_file(selected_images) + local current_image, run_cmd = "", "" + for _, image in pairs(selected_images) do + current_image = image.path..PS..image.filename + run_cmd = string.format(open_files[act_os], df.sanitize_filename(current_image)) + dt.print_log("OpenInExplorer run_cmd = "..run_cmd) + dsys.external_command(run_cmd) + end +end + +--Create a link for each selected image, and finally call the file manager. +local function set_links(selected_images) + local current_image, link_target, run_cmd, k = "", "", "", nil + for k, image in pairs(selected_images) do + current_image = image.path..PS..image.filename + link_target = df.create_unique_filename(links_dir .. PS .. image.filename) + run_cmd = string.format("ln -s %s %s", df.sanitize_filename(current_image), df.sanitize_filename(link_target)) + --[[ + In case Windows will allow normal users to create soft links: + if act_os == "windows" then + run_cmd = string.format("mklink %s %s", df.sanitize_filename(link_target), df.sanitize_filename(current_image)) + end + ]] + if dsys.external_command(run_cmd) ~= 0 then + dt.print(_("failed to create links, missing rights?")) + dt.print_error("OpenInExplorer: Failed to create links") + return + end + end + --The URI format is necessary only for the Linux busctl command. + --But it is accepted by the Windows Explorer and macOS's Finder all the same. + run_cmd = string.format(open_dir[act_os], df.sanitize_filename("file://"..links_dir)) + dt.print_log("OpenInExplorer run_cmd = "..run_cmd) + dsys.external_command(run_cmd) +end + +--The working function that starts the particular task. +local function open_in_fmanager() + local images = dt.gui.selection() + if #images == 0 then + dt.print(_("please select an image")) + else + if use_links and not check_if_links_dir_exists() then + return + end + if #images > 15 and not use_links then + dt.print(_("please select fewer images (max. 15)")) + elseif use_links then + set_links(images) + 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 + end + end +end + +local function destroy() + dt.gui.libs.image.destroy_action("OpenInExplorer") + dt.destroy_event("OpenInExplorer", "shortcut") + if act_os ~= "windows" then + dt.preferences.destroy("OpenInExplorer", "linked_image_files_dir") + end + dt.preferences.destroy("OpenInExplorer", "use_links") +end + + +-- GUI -- +dt.gui.libs.image.register_action( + "OpenInExplorer", _("show in file explorer"), + function() open_in_fmanager() end, + _("open the file manager at the selected image's location") +) + + +if act_os ~= "windows" then + dt.preferences.register("OpenInExplorer", "linked_image_files_dir", -- name + "directory", -- type + _("OpenInExplorer: linked files directory"), -- label + _("directory to store the links to the file names, requires restart to take effect"), -- tooltip + "Links to image files", -- default + dt.new_widget("file_chooser_button"){ + title = _("select directory"), + is_directory = true, + } + ) + dt.preferences.register("OpenInExplorer", "use_links", -- name + "bool", -- type + _("OpenInExplorer: use links"), -- label + _("use links instead of multiple windows, requires restart to take effect"), -- tooltip + false, -- default + "" + ) +end + +dt.register_event( + "OpenInExplorer", "shortcut", + function(event, shortcut) open_in_fmanager() end, + "OpenInExplorer" +) + +script_data.destroy = destroy + +return script_data diff --git a/contrib/RL_out_sharp.lua b/contrib/RL_out_sharp.lua index 40cfb204..b65581c9 100644 --- a/contrib/RL_out_sharp.lua +++ b/contrib/RL_out_sharp.lua @@ -65,31 +65,50 @@ local MODULE_NAME = "RL_out_sharp" -- check API version du.check_min_api_version("7.0.0", MODULE_NAME) +-- translation +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) + end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them -- OS compatibility local PS = dt.configuration.running_os == "windows" and "\\" or "/" --- translation -local gettext = dt.gettext -gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS) -local function _(msgid) - return gettext.dgettext(MODULE_NAME, msgid) - end - -- initialize module preferences if not dt.preferences.read(MODULE_NAME, "initialized", "bool") then dt.preferences.write(MODULE_NAME, "sigma", "string", "0.7") 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 --------------------------------------------------------------- @@ -136,7 +155,7 @@ local function export2RL(storage, image_table, extra_data) for image, temp_name in pairs(image_table) do i = i + 1 - dt.print(_("sharpening image ")..i.." ...") + dt.print(string.format(_("sharpening image %d ..."), i)) -- create unique filename new_name = output_folder..PS..df.get_basename(temp_name)..".jpg" new_name = df.create_unique_filename(new_name) @@ -153,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 64de2a80..32add29b 100644 --- a/contrib/autostyle.lua +++ b/contrib/autostyle.lua @@ -39,16 +39,33 @@ 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 + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +local have_not_printed_config_message = true + +script_data.metadata = { + 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/" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them -- run command and retrieve stdout local function get_stdout(cmd) @@ -86,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("EXIF TAG not found in " .. pref) - return 0 - end - if (not value) then - darktable.print("value to match not found in " .. pref) - return 0 - end - if (not style_name) then - darktable.print("style name not found in " .. 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("style not found for autostyle: " .. 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("Couldn't get attribute " .. auto_dr_attr .. " from exiftool's output") - 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 @@ -153,7 +176,7 @@ local function autostyle_apply(shortcut) images_submitted = images_submitted + 1 images_processed = images_processed + autostyle_apply_one_image(image) end - darktable.print("Applied auto style to " .. images_processed .. " out of " .. images_submitted .. " image(s)") + darktable.print(string.format(_("applied auto style to %d out of %d image(s)"), images_processed, images_submitted)) end local function destroy() @@ -163,13 +186,15 @@ end -- Registering events darktable.register_event("autostyle", "shortcut", autostyle_apply, - "Apply your chosen style from exiftool tags") + _("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 07ce8119..0e6f66e6 100644 --- a/contrib/change_group_leader.lua +++ b/contrib/change_group_leader.lua @@ -36,29 +36,34 @@ USAGE local dt = require "darktable" local du = require "lib/dtutils" -local gettext = dt.gettext +local gettext = dt.gettext.gettext local MODULE = "change_group_leader" du.check_min_api_version("3.0.0", MODULE) +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain(MODULE, dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext(MODULE, msgid) -end +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 @@ -75,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 @@ -120,7 +125,7 @@ end local function process_image_groups(images) if #images < 1 then - dt.print(_("No images selected.")) + dt.print(_("no images selected")) dt.print_log(MODULE .. "no images seletected, returning...") else local mode = cgl.widgets.mode.value @@ -160,7 +165,7 @@ cgl.widgets.mode = dt.new_widget("combobox"){ } cgl.widgets.execute = dt.new_widget("button"){ - label = _("Execute"), + label = _("execute"), clicked_callback = function() process_image_groups(dt.gui.action_images) end @@ -197,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 518e0372..2663ab4b 100644 --- a/contrib/clear_GPS.lua +++ b/contrib/clear_GPS.lua @@ -39,28 +39,33 @@ local dt = require "darktable" local du = require "lib/dtutils" +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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/" +} + 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 - -local gettext = dt.gettext +script_data.show = nil -- only required for libs since the destroy_method only hides them -- not a number local NaN = 0/0 du.check_min_api_version("7.0.0", "clear_GPS") --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("clear_GPS",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("clear_GPS", msgid) -end - local function clear_GPS(images) for _, image in ipairs(images) do -- set the location information to Not a Number (NaN) so it displays correctly @@ -80,13 +85,13 @@ script_data.destroy = destroy dt.gui.libs.image.register_action( "clear_GPS", _("clear GPS data"), function(event, images) clear_GPS(images) end, - _("Clear GPS data from selected images") + _("clear GPS data from selected images") ) 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 bd097558..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") @@ -52,13 +53,10 @@ du.check_min_api_version("7.0.0", "color_profile_manager") -- L O C A L I Z A T I O N -- - - - - - - - - - - - - - - - - - - - - - - - -local gettext = dt.gettext - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("color_profile_manager", dt.configuration.config_dir .. "/lua/locale/") +local gettext = dt.gettext.gettext local function _(msgid) - return gettext.dgettext("color_profile_manager", msgid) + return gettext(msgid) end -- - - - - - - - - - - - - - - - - - - - - - - - @@ -119,13 +117,13 @@ end local function add_profile(file, dir) df.file_copy(file, dir) - dt.print(_("added color profile " .. file .. " to " .. dir)) + dt.print(string.format(_("added color profile %s to %s"), file, dir)) dt.print_log("color profile " .. file .. " added to " .. dir) end local function remove_profile(file, dir) os.remove(dir .. PS .. file) - dt.print(_("removed color profile " .. file .. " from " .. dir)) + dt.print(string.format(_("removed color profile %s from %s"), file, dir)) dt.print_log("color profile " .. file .. " removed from " .. dir) end @@ -364,6 +362,13 @@ end local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = destroy script_data.restart = restart script_data.destroy_method = "hide" diff --git a/contrib/copy_attach_detach_tags.lua b/contrib/copy_attach_detach_tags.lua index dda92aed..3f1f6d34 100644 --- a/contrib/copy_attach_detach_tags.lua +++ b/contrib/copy_attach_detach_tags.lua @@ -40,24 +40,29 @@ local dt = require "darktable" local du = require "lib/dtutils" local debug = require "darktable.debug" -local gettext = dt.gettext +local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "copy_attach_detach_tags") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("copy_attach_detach_tags",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("copy_attach_detach_tags", msgid) -end +script_data.show = nil -- only required for libs since the destroy_method only hides them local cadt = {} cadt.module_installed = false @@ -98,7 +103,7 @@ local function mcopy_tags() end end - dt.print(_('Image tags copied ...')) + dt.print(_('image tags copied ...')) --create UI tag list local taglist = "" @@ -120,7 +125,7 @@ local function mcopy_tags() local function attach_tags() if next(image_tags) == nil then - dt.print(_('No tag to attach, please copy tags first.')) + dt.print(_('no tag to attach, please copy tags first.')) return true end @@ -144,7 +149,7 @@ local function attach_tags() end end end - dt.print(_('Tags attached ...')) + dt.print(_('tags attached ...')) end local function detach_tags() @@ -160,13 +165,13 @@ local function detach_tags() end end end - dt.print(_('Tags removed from image(s).')) + dt.print(_('tags removed from image(s).')) end local function replace_tags() detach_tags() attach_tags() - dt.print(_('Tags replaced')) + dt.print(_('tags replaced')) end local function install_module() diff --git a/contrib/cr2hdr.lua b/contrib/cr2hdr.lua index 25d07a2f..36f325f2 100644 --- a/contrib/cr2hdr.lua +++ b/contrib/cr2hdr.lua @@ -37,13 +37,27 @@ local du = require "lib/dtutils" du.check_min_api_version("7.0.0", "cr2hdr") +local gettext = darktable.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them local queue = {} local processed_files = {} @@ -86,7 +100,7 @@ end local function convert_images() if next(queue) == nil then return end - job = darktable.gui.create_job("Dual ISO conversion", true, stop_conversion) + job = darktable.gui.create_job(_("dual ISO conversion"), true, stop_conversion) for key,image in pairs(queue) do if job.valid then job.percent = (key-1)/#queue @@ -97,7 +111,7 @@ local function convert_images() end local success_count = 0 for _ in pairs(processed_files) do success_count = success_count + 1 end - darktable.print("Dual ISO conversion successful on " .. success_count .. "/" .. #queue .. " images.") + darktable.print(string.format(_("dual ISO conversion successful on %d/%d images."), success_count, #queue)) job.valid = false processed_files = {} queue = {} @@ -121,13 +135,13 @@ local function destroy() end darktable.register_event("cr2hdr", "shortcut", - convert_action_images, "Run cr2hdr (Magic Lantern DualISO converter) on selected images") + convert_action_images, _("run cr2hdr (Magic Lantern DualISO converter) on selected images")) darktable.register_event("cr2hdr", "post-import-image", file_imported) darktable.register_event("cr2hdr", "post-import-film", film_imported) -darktable.preferences.register("cr2hdr", "onimport", "bool", "Invoke on import", "If true then cr2hdr will try to proccess every file during importing. Warning: cr2hdr is quite slow even in figuring out on whether the file is Dual ISO or not.", false) +darktable.preferences.register("cr2hdr", "onimport", "bool", _("invoke on import"), _("if true then cr2hdr will try to proccess every file during importing\nwarning: cr2hdr is quite slow even in figuring out on whether the file is dual ISO or not."), false) script_data.destroy = destroy diff --git a/contrib/cycle_group_leader.lua b/contrib/cycle_group_leader.lua new file mode 100644 index 00000000..e168e93b --- /dev/null +++ b/contrib/cycle_group_leader.lua @@ -0,0 +1,162 @@ +--[[ + + cycle_group_leader.lua - change image group leader + + 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 . +]] +--[[ + cycle_group_leader - change image grouip leader + + cycle_group_leader changes the group leader to the next + image in the group. If the end of the group is reached + then the next image is wrapped around to the first image. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + None + + USAGE + * enable with script_manager + * assign a key to the shortcut + + BUGS, COMMENTS, SUGGESTIONS + Bill Ferguson + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "cycle_group_leader" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("7.0.0", MODULE) + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- 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.metadata = { + 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" +} + +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 + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function toggle_global_toolbox_grouping() + dt.gui.libs.global_toolbox.grouping = false + dt.gui.libs.global_toolbox.grouping = true +end + +local function hinter_msg(msg) + dt.print_hinter(msg) + dt.control.sleep(1500) + dt.print_hinter(" ") +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N P R O G R A M +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function cycle_group_leader(image) + local group_images = image:get_group_members() + if #group_images < 2 then + hinter_msg(_("no images to cycle through in group")) + return + else + local position = nil + for i, img in ipairs(group_images) do + if image == img then + position = i + end + end + + if position == #group_images then + position = 1 + else + position = position + 1 + end + + new_leader = group_images[position] + new_leader:make_group_leader() + dt.gui.selection({new_leader}) + + if dt.gui.libs.global_toolbox.grouping then + -- toggle the grouping to make the new leader show + toggle_global_toolbox_grouping() + end + end +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + -- put things to destroy (events, storages, etc) here + dt.destroy_event(MODULE, "shortcut") +end + +script_data.destroy = destroy + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.register_event(MODULE, "shortcut", + function(event, shortcut) + -- ignore the film roll, it contains all the images, not just the imported + local images = dt.gui.selection() + if #images < 1 then + dt.print(_("no image selected, please select an image and try again")) + else + cycle_group_leader(images[1]) + end + end, + _("cycle group leader") +) + +return script_data 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 3103821c..5027c564 100644 --- a/contrib/enfuseAdvanced.lua +++ b/contrib/enfuseAdvanced.lua @@ -67,20 +67,28 @@ if dt.configuration.running_os == 'windows' then os_path_seperator = '\\' end 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 + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -local gettext = dt.gettext -gettext.bindtextdomain('enfuseAdvanced',dt.configuration.config_dir..'/lua/locale/') -local function _(msgid) - return gettext.dgettext('enfuseAdvanced', msgid) -end +script_data.show = nil -- only required for libs since the destroy_method only hides them -- INITS -- local AIS = { @@ -259,7 +267,7 @@ local function ExeUpdate(prog_tbl) --updates executable paths and verifies them if not prog.bin then prog.install_error = true dt.preferences.write(mod, 'bin_exists', 'bool', false) - dt.print(_('issue with ')..prog.name.._(' executable')) + dt.print(string.format(_("issue with %s executable"), prog.name)) else prog.bin = CleanSpaces(prog.bin) end @@ -376,7 +384,7 @@ local function SaveToPreference(preset) --save the present values of enfuse GUI dt.preferences.write(mod, preset..argument, arg_data.style, temp) end end - dt.print(_('saved to ')..preset) + dt.print(string.format(_("saved to %s"), preset)) end local function LoadFromPreference(preset) --load values from the specified 'preset' into the GUI elements @@ -392,7 +400,7 @@ local function LoadFromPreference(preset) --load values from the specified 'pres dt.preferences.write(mod, 'active_'..argument, arg_data.style, temp) end end - dt.print(_('loaded from ')..preset) + dt.print(string.format(_("loaded from %s"), preset)) end local function remove_temp_files(images_to_remove) --deletes all files specified by the input string @@ -427,7 +435,7 @@ local function support_format(storage, format) --tells dt we only support TIFF e end local function show_status(storage, image, format, filename, number, total, high_quality, extra_data) --outputs message to user showing script export status - dt.print(_('export for image fusion ')..tostring(math.floor(number))..' / '..tostring(math.floor(total))) + dt.print(string.format(_("export for image fusion %d / %d"), math.floor(number), math.floor(total))) end local function main(storage, image_table, extra_data) @@ -435,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 = '' @@ -455,8 +463,8 @@ local function main(storage, image_table, extra_data) job.valid = false if resp ~= 0 then remove_temp_files(images_to_remove) - dt.print_error(AIS.name.._(' failed')) - dt.print(AIS.name.._(' failed')) + dt.print(string.format(_("%s failed"), AIS.name)) + dt.print_error(AIS.name .. ' failed') return end end @@ -482,8 +490,8 @@ local function main(storage, image_table, extra_data) local resp = dsys.external_command(run_cmd) if resp ~= 0 then remove_temp_files(images_to_remove) - dt.print_error(ENF.name.._(' failed')) - dt.print(ENF.name.._(' failed')) + dt.print_error(ENF.name..' failed') + dt.print(string.format(_("%s failed"), ENF.name)) return end @@ -728,7 +736,7 @@ GUI.ENF.hard_masks = dt.new_widget('check_button'){ GUI.ENF.save_masks = dt.new_widget('check_button'){ label = _('save masks'), value = dt.preferences.read(mod, 'active_save_masks', 'bool'), - tooltip = _('Save the generated weight masks to your home directory,\nenblend saves masks as 8 bit grayscale, \ni.e. single channel images. \nfor accuracy we recommend to choose a lossless format.'), + tooltip = _('save the generated weight masks to your home directory,\nenblend saves masks as 8 bit grayscale, \ni.e. single channel images. \nfor accuracy we recommend to choose a lossless format.'), clicked_callback = function(self) dt.preferences.write(mod, 'active_save_masks', 'bool', self.value) end, reset_callback = function(self) self.value = false end } @@ -881,7 +889,7 @@ GUI.Target.output_directory = dt.new_widget('file_chooser_button'){ GUI.Target.source_location = dt.new_widget('check_button'){ label = _('save to source image location'), value = dt.preferences.read(mod, 'active_source_location', 'bool'), - tooltip = _('If checked ignores the location above and saves output image(s) to the same location as the source images.'), + tooltip = _('if checked ignores the location above and saves output image(s) to the same location as the source images.'), clicked_callback = function(self) dt.preferences.write(mod, 'active_source_location', 'bool', self.value) end, reset_callback = function(self) self.value = true end } @@ -915,8 +923,8 @@ GUI.Target.auto_import = dt.new_widget('check_button'){ } temp = dt.preferences.read(mod, 'active_apply_style_ind', 'integer') GUI.Target.apply_style = dt.new_widget('combobox'){ - label = _('Apply Style on Import'), - tooltip = _('Apply selected style on auto-import to newly created blended image'), + label = _('apply style on Import'), + tooltip = _('apply selected style on auto-import to newly created blended image'), selected = 1, 'none', changed_callback = function(self) @@ -937,16 +945,16 @@ GUI.Target.apply_style.selected = temp GUI.Target.copy_tags = dt.new_widget('check_button'){ label = _('copy tags'), value = dt.preferences.read(mod, 'active_copy_tags', 'bool'), - tooltip = _('Copy tags from first image.'), + tooltip = _('copy tags from first image.'), clicked_callback = function(self) dt.preferences.write(mod, 'active_copy_tags', 'bool', self.value) end, reset_callback = function(self) self.value = true end } 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, 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') @@ -1013,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, @@ -1021,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, @@ -1029,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 3d023901..9ec29544 100644 --- a/contrib/exportLUT.lua +++ b/contrib/exportLUT.lua @@ -33,21 +33,27 @@ local ds = require("lib/dtutils.system") du.check_min_api_version("7.0.0", "exportLUT") +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - -local gettext = dt.gettext - -gettext.bindtextdomain("exportLUT",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("exportLUT", msgid) -end +script_data.show = nil -- only required for libs since the destroy_method only hides them du.check_min_api_version("5.0.0", "exportLUT") @@ -63,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 } @@ -105,9 +111,9 @@ end local function export_luts() local identity = dt.database.import(file_chooser_button.value) if(type(identity) ~= "userdata") then - dt.print(_("Invalid identity lut file")) + dt.print(_("invalid identity lut file")) else - local job = dt.gui.create_job(_('Exporting styles as haldCLUTs'), true, end_job) + local job = dt.gui.create_job(_('exporting styles as haldCLUTs'), true, end_job) local size = 1 @@ -126,9 +132,9 @@ local function export_luts() io_lut:write_image(identity, output_path(style.name, job)) count = count + 1 job.percent = count / size - dt.print(_("Exported: ") .. output_path(style.name, job)) + dt.print(string.format(_("exported: %s"), output_path(style.name, job))) end - dt.print(_("Done exporting haldCLUTs")) + dt.print(_("done exporting haldCLUTs")) job.valid = false identity:reset() end diff --git a/contrib/ext_editor.lua b/contrib/ext_editor.lua index 10d1ca6c..be371e92 100644 --- a/contrib/ext_editor.lua +++ b/contrib/ext_editor.lua @@ -75,13 +75,28 @@ local dtsys = require "lib/dtutils.system" local MODULE_NAME = "ext_editor" du.check_min_api_version("7.0.0", MODULE_NAME) +-- translation +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them -- OS compatibility local PS = dt.configuration.running_os == "windows" and "\\" or "/" @@ -93,13 +108,6 @@ ee.event_registered = false ee.widgets = {} --- translation -local gettext = dt.gettext -gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS) -local function _(msgid) - return gettext.dgettext(MODULE_NAME, msgid) -end - -- maximum number of external programs, can be increased to necessity local MAX_EDITORS = 9 @@ -155,7 +163,7 @@ local function UpdateProgramList(combobox, button_edit, button_edit_copy, update button_edit.sensitive = active button_edit_copy.sensitive = active - if update_button_pressed then dt.print(n_entries.._(" editors configured")) end + if update_button_pressed then dt.print(string.format(_("%d editors configured"), n_entries)) end end @@ -222,17 +230,17 @@ local function OpenWith(images, choice, copy) -- physical copy, check result, return if error local copy_success = df.file_copy(name, new_name) if not copy_success then - dt.print(_("error copying file ")..name) + dt.print(string.format(_("error copying file %s"), name)) return end end -- launch the external editor, check result, return if error local run_cmd = bin.." "..df.sanitize_filename(new_name) - dt.print(_("launching ")..friendly_name.."...") + dt.print(string.format(_("launching %s..."), friendly_name)) local result = dtsys.external_command(run_cmd) if result ~= 0 then - dt.print(_("error launching ")..friendly_name) + dt.print(string.format(_("error launching %s"), friendly_name)) return end @@ -325,7 +333,7 @@ local function export2collection(storage, image_table, extra_data) -- move image to collection folder, check result, return if error move_success = df.file_move(temp_name, new_name) if not move_success then - dt.print(_("error moving file ")..temp_name) + dt.print(string.format(_("error moving file %s"), temp_name)) return end @@ -377,7 +385,7 @@ end local function restart() for i = 1, MAX_EDITORS do dt.register_event(MODULE_NAME .. i, "shortcut", - program_shortcut, _("edit with program ")..string.format("%02d", i)) + program_shortcut, string.format(_("edit with program %02d"), i)) end dt.register_storage("exp2coll", _("collection"), nil, export2collection) dt.gui.libs[MODULE_NAME].visible = true @@ -475,11 +483,11 @@ dt.register_storage("exp2coll", _("collection"), nil, export2collection) -- register the new preferences ----------------------------------------------- for i = MAX_EDITORS, 1, -1 do dt.preferences.register(MODULE_NAME, "program_path_"..i, "file", - _("executable for external editor ")..i, - _("select executable for external editor") , _("(None)")) + string.format(_("executable for external editor %d"), i), + _("select executable for external editor") , _("(none)")) dt.preferences.register(MODULE_NAME, "program_name_"..i, "string", - _("name of external editor ")..i, + string.format(_("name of external editor %d"), i), _("friendly name of external editor"), "") end dt.preferences.register(MODULE_NAME, "show_in_darkrooom", "bool", @@ -490,7 +498,7 @@ dt.preferences.register(MODULE_NAME, "show_in_darkrooom", "bool", -- register the new shortcuts ------------------------------------------------- for i = 1, MAX_EDITORS do dt.register_event(MODULE_NAME .. i, "shortcut", - program_shortcut, _("edit with program ")..string.format("%02d", i)) + program_shortcut, string.format(_("edit with program %02d"), i)) end diff --git a/contrib/face_recognition.lua b/contrib/face_recognition.lua index 68a497cc..0c60a5c1 100644 --- a/contrib/face_recognition.lua +++ b/contrib/face_recognition.lua @@ -44,7 +44,7 @@ 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 +local gettext = dt.gettext.gettext -- constants @@ -54,13 +54,25 @@ local OUTPUT = dt.configuration.tmp_dir .. PS .. "facerecognition.txt" du.check_min_api_version("7.0.0", MODULE) +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them -- namespace @@ -68,13 +80,6 @@ local fc = {} fc.module_installed = false fc.event_registered = false --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("face_recognition", dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("face_recognition", msgid) -end - local function build_image_table(images) local image_table = {} local file_extension = "" @@ -134,7 +139,7 @@ local function do_export(img_tbl, images) job.percent = 0.0 for export,img in pairs(img_tbl) do exp_cnt = exp_cnt + 1 - dt.print(string.format(_("Exporting image %i of %i images"), exp_cnt, images)) + dt.print(string.format(_("exporting image %i of %i images"), exp_cnt, images)) exporter:write_image(img, export, upsize) job.percent = job.percent + percent_step end @@ -204,7 +209,7 @@ local function face_recognition () local bin_path = df.check_if_bin_exists("face_recognition") if not bin_path then - dt.print(_("Face recognition not found")) + dt.print(_("face recognition not found")) return end @@ -256,7 +261,7 @@ local function face_recognition () local command = bin_path .. " --cpus " .. nrCores .. " --tolerance " .. tolerance .. " " .. knownPath .. " " .. path .. " > " .. OUTPUT os.setlocale() dt.print_log("Face recognition: Running command: " .. command) - dt.print(_("Starting face recognition...")) + dt.print(_("starting face recognition...")) dtsys.external_command(command) @@ -264,9 +269,9 @@ local function face_recognition () local f = io.open(OUTPUT, "rb") if not f then - dt.print(_("Face recognition failed")) + dt.print(_("face recognition failed")) else - dt.print(_("Face recognition finished")) + dt.print(_("face recognition finished")) f:close () end @@ -398,7 +403,7 @@ fc.category_tags = dt.new_widget("entry"){ fc.tolerance = dt.new_widget("slider"){ label = _("tolerance"), - tooltip = ("detection tolerance - 0.6 default - lower if too many faces detected"), + tooltip = _("detection tolerance - 0.6 default - lower if too many faces detected"), soft_min = 0.0, hard_min = 0.0, soft_max = 1.0, @@ -481,12 +486,12 @@ table.insert(widgets, fc.num_cores) table.insert(widgets, fc.export_format) table.insert(widgets, dt.new_widget("box"){ orientation = "horizontal", - dt.new_widget("label"){ label = _("width ")}, + dt.new_widget("label"){ label = _("width")}, fc.width, }) table.insert(widgets, dt.new_widget("box"){ orientation = "horizontal", - dt.new_widget("label"){ label = _("height ")}, + dt.new_widget("label"){ label = _("height")}, fc.height, }) table.insert(widgets, fc.execute) diff --git a/contrib/fujifilm_dynamic_range.lua b/contrib/fujifilm_dynamic_range.lua index 68694ea3..37f79511 100644 --- a/contrib/fujifilm_dynamic_range.lua +++ b/contrib/fujifilm_dynamic_range.lua @@ -60,16 +60,30 @@ 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") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them local function detect_dynamic_range(event, image) if image.exif_maker ~= "FUJIFILM" then @@ -110,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 18dba2bc..b29996d9 100644 --- a/contrib/fujifilm_ratings.lua +++ b/contrib/fujifilm_ratings.lua @@ -26,51 +26,61 @@ Dependencies: local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" -local gettext = dt.gettext +local dtsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "fujifilm_ratings") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - -gettext.bindtextdomain("fujifilm_ratings", dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("fujifilm_ratings", msgid) -end +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(_("Using JPEG Rating: ") .. tostring(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(_("Using RAF Rating: ") .. tostring(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 cc3d9560..93e77289 100644 --- a/contrib/geoJSON_export.lua +++ b/contrib/geoJSON_export.lua @@ -35,24 +35,30 @@ USAGE local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" -local gettext = dt.gettext +local dtsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "geoJSON_export") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("geoJSON_export",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("geoJSON_export", msgid) -end +script_data.show = nil -- only required for libs since the destroy_method only hides them -- Sort a table local function spairs(_table, order) -- Code copied from http://stackoverflow.com/questions/15706270/sort-a-table-in-lua @@ -79,24 +85,24 @@ local function spairs(_table, order) -- Code copied from http://stackoverflow.co end local function show_status(storage, image, format, filename, number, total, high_quality, extra_data) - dt.print(string.format(_("Export Image %i/%i"), number, total)) + dt.print(string.format(_("export image %i/%i"), number, total)) 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 @@ -287,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 @@ -307,20 +313,20 @@ end dt.preferences.register("geoJSON_export", "CreateMapBoxHTMLFile", "bool", - _("geoJSON export: Create an additional HTML file"), - _("Creates a HTML file, that loads the geoJASON file. (Needs a MapBox key"), + "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"), - _("Opens the geoJSON file after the export with the standard program for geoJSON files"), + "geoJSON export: " .. _("open geoJSON file after export"), + _("opens the geoJSON file after the export with the standard program for geoJSON files"), false ) local handle = io.popen("xdg-user-dir DESKTOP") @@ -332,8 +338,8 @@ end dt.preferences.register("geoJSON_export", "ExportDirectory", "directory", - _("geoJSON export: Export directory"), - _("A directory that will be used to export the geoJSON files"), + _("geoJSON export: export directory"), + _("a directory that will be used to export the geoJSON files"), result ) -- Register diff --git a/contrib/geoToolbox.lua b/contrib/geoToolbox.lua index db99d26f..01a14eea 100644 --- a/contrib/geoToolbox.lua +++ b/contrib/geoToolbox.lua @@ -28,24 +28,30 @@ require "geoToolbox" local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" -local gettext = dt.gettext +local dtsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "geoToolbox") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + name = _("geo toolbox"), + purpose = _("geodata tools"), + author = "Tobias Jakobs", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/geoToolbox" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("geoToolbox",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("geoToolbox", msgid) -end +script_data.show = nil -- only required for libs since the destroy_method only hides them local gT = {} @@ -55,21 +61,21 @@ gT.event_registered = false -- local labelDistance = dt.new_widget("label") -labelDistance.label = _("Distance:") +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 } -- @@ -161,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 @@ -200,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 @@ -236,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 @@ -283,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 @@ -300,7 +306,7 @@ local function copy_gps() end label_copy_gps_lat.label = _("latitude: ") .. copy_gps_latitude - label_copy_gps_lon.label = _("longitude: ") ..copy_gps_longitude + label_copy_gps_lon.label = _("longitude: ") .. copy_gps_longitude label_copy_gps_ele.label = _("elevation: ") .. copy_gps_elevation return @@ -310,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 @@ -337,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 @@ -364,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 @@ -380,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 @@ -455,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 @@ -487,12 +493,12 @@ 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) + return string.format(_("distance: %.2f %s"), distance, distanceUnit) end local function print_calc_distance() @@ -515,12 +521,12 @@ local altitude_filename = dt.new_widget("entry") text = "altitude.csv", placeholder = "altitude.csv", editable = true, - tooltip = _("Name of the exported file"), + tooltip = _("name of the exported file"), reset_callback = function(self) self.text = "text" end } local function altitude_profile() - dt.print(_("Start export")) + dt.print(_("start export")) local sel_images = dt.gui.action_images local lat1 = 0; @@ -539,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 @@ -581,7 +587,7 @@ local function altitude_profile() file = io.open(exportDirectory.."/"..exportFilename, "w") file:write(csv_file) file:close() - dt.print(_("File created in ")..exportDirectory) + dt.print(string.format(_("file created in %s"), exportDirectory)) end @@ -589,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 @@ -611,14 +617,14 @@ end local function restart() dt.register_event("geoToolbox_cd", "shortcut", - print_calc_distance, _("Calculate the distance from latitude and longitude in km")) + print_calc_distance, _("calculate the distance from latitude and longitude in km")) dt.register_event("geoToolbox", "mouse-over-image-changed", toolbox_calc_distance) dt.register_event("geoToolbox_wg", "shortcut", - select_with_gps, _("Select all images with GPS information")) + select_with_gps, _("select all images with GPS information")) dt.register_event("geoToolbox_ng", "shortcut", - select_without_gps, _("Select all images without GPS information")) + select_without_gps, _("select all images without GPS information")) dt.gui.libs["geoToolbox"].visible = true end @@ -641,20 +647,20 @@ gT.widget = dt.new_widget("box") dt.new_widget("button") { label = _("select geo images"), - tooltip = _("Select all images with GPS information"), + tooltip = _("select all images with GPS information"), clicked_callback = select_with_gps }, dt.new_widget("button") { label = _("select non-geo images"), - tooltip = _("Select all images without GPS information"), + tooltip = _("select all images without GPS information"), clicked_callback = select_without_gps }, separator,-------------------------------------------------------- dt.new_widget("button") { label = _("copy GPS data"), - tooltip = _("Copy GPS data"), + tooltip = _("copy GPS data"), clicked_callback = copy_gps }, label_copy_gps_lat, @@ -663,7 +669,7 @@ gT.widget = dt.new_widget("box") dt.new_widget("button") { label = _("paste GPS data"), - tooltip = _("Paste GPS data"), + tooltip = _("paste GPS data"), clicked_callback = paste_gps }, separator2,-------------------------------------------------------- @@ -693,14 +699,14 @@ gT.widget = dt.new_widget("box") dt.new_widget("button") { label = _("open in Gnome Maps"), - tooltip = _("Open location in Gnome Maps"), + tooltip = _("open location in Gnome Maps"), clicked_callback = open_location_in_gnome_maps }, separator4,-------------------------------------------------------- dt.new_widget("button") { label = _("reverse geocode"), - tooltip = _("This just shows the name of the location, but doesn't add it as tag"), + tooltip = _("this just shows the name of the location, but doesn't add it as tag"), clicked_callback = reverse_geocode }, separator5,-------------------------------------------------------- @@ -710,7 +716,7 @@ gT.widget = dt.new_widget("box") dt.new_widget("button") { label = _("export altitude CSV file"), - tooltip = _("Create an altitude profile using the GPS data in the metadata"), + tooltip = _("create an altitude profile using the GPS data in the metadata"), clicked_callback = altitude_profile }, labelDistance @@ -738,19 +744,19 @@ 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 dt.register_event("geoToolbox_cd", "shortcut", - print_calc_distance, _("Calculate the distance from latitude and longitude in km")) + print_calc_distance, _("calculate the distance from latitude and longitude in km")) dt.register_event("geoToolbox", "mouse-over-image-changed", toolbox_calc_distance) dt.register_event("geoToolbox_wg", "shortcut", - select_with_gps, _("Select all images with GPS information")) + select_with_gps, _("select all images with GPS information")) dt.register_event("geoToolbox_ng", "shortcut", - select_without_gps, _("Select all images without GPS information")) + select_without_gps, _("select all images without GPS information")) script_data.destroy = destroy script_data.restart = restart diff --git a/contrib/gimp.lua b/contrib/gimp.lua index d2809222..92c6d0ce 100644 --- a/contrib/gimp.lua +++ b/contrib/gimp.lua @@ -69,25 +69,30 @@ 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 +local gettext = dt.gettext.gettext local gimp_widget = nil du.check_min_api_version("7.0.0", "gimp") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("gimp",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("gimp", msgid) -end +script_data.show = nil -- only required for libs since the destroy_method only hides them local function group_if_not_member(img, new_img) local image_table = img:get_group_members() @@ -108,7 +113,7 @@ end local function show_status(storage, image, format, filename, number, total, high_quality, extra_data) - dt.print(string.format(_("Export Image %i/%i"), number, total)) + dt.print(string.format(_("export image %i/%i"), number, total)) end local function gimp_edit(storage, image_table, extra_data) --finalize @@ -118,7 +123,7 @@ local function gimp_edit(storage, image_table, extra_data) --finalize local gimp_executable = df.check_if_bin_exists("gimp") if not gimp_executable then - dt.print_error(_("GIMP not found")) + dt.print_error("GIMP not found") return end @@ -141,7 +146,7 @@ local function gimp_edit(storage, image_table, extra_data) --finalize img_list = img_list ..exp_img.. " " end - dt.print(_("Launching GIMP...")) + dt.print(_("launching GIMP...")) local gimpStartCommand gimpStartCommand = gimp_executable .. " " .. img_list @@ -212,7 +217,7 @@ gimp_widget = dt.new_widget("check_button"){ end } -dt.register_storage("module_gimp", _("Edit with GIMP"), show_status, gimp_edit, nil, nil, gimp_widget) +dt.register_storage("module_gimp", _("edit with GIMP"), show_status, gimp_edit, nil, nil, gimp_widget) -- script_data.destroy = destroy diff --git a/contrib/gpx_export.lua b/contrib/gpx_export.lua index 152d3e60..b310f3a9 100644 --- a/contrib/gpx_export.lua +++ b/contrib/gpx_export.lua @@ -25,24 +25,29 @@ 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") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("gpx_export",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("gpx_export", msgid) -end +script_data.show = nil -- only required for libs since the destroy_method only hides them local gpx = {} gpx.module_installed = false @@ -131,11 +136,11 @@ local function create_gpx_file() local file = io.open(path, "w") if (file == nil) then - dt.print(_("invalid path: ")..path) + dt.print(string.format(_("invalid path: %s"), path)) else file:write(gpx_file) file:close() - dt.print(_("gpx file created: ")..path) + dt.print(string.format(_("gpx file created: "), path)) end end @@ -143,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 @@ -203,5 +208,6 @@ 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/harmonic_armature_guide.lua b/contrib/harmonic_armature_guide.lua index e5c23f27..ffe03a29 100644 --- a/contrib/harmonic_armature_guide.lua +++ b/contrib/harmonic_armature_guide.lua @@ -32,17 +32,28 @@ 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", "harmonic_armature_guide") --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("harmonic_armature_guide",dt.configuration.config_dir.."/lua/locale/") - local function _(msgid) - return gettext.dgettext("harmonic_armature_guide", msgid) + return gettext(msgid) end +local script_data = {} + +script_data.metadata = { + 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" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + dt.guides.register_guide("harmonic armature", -- draw function(cairo, x, y, width, height, zoom_scale) @@ -75,4 +86,12 @@ end, function() return dt.new_widget("label"){label = _("harmonic armature"), halign = "start"} end -) \ No newline at end of file +) + +local function destroy() + -- nothing to destroy +end + +script_data.destroy = destroy + +return script_data diff --git a/contrib/hif_group_leader.lua b/contrib/hif_group_leader.lua new file mode 100644 index 00000000..412eea07 --- /dev/null +++ b/contrib/hif_group_leader.lua @@ -0,0 +1,200 @@ +--[[ + + hif_group_leader.lua - Make hif image group leader + + 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 . +]] +--[[ + hif_group_leader - Make hif image group leader + + After a film roll is imported, check for RAW-JPG image groups + and make the JPG image the group leader. This is on by default + but can be disabled in preferences. + + Shortcuts are included to filter existing collections or + selections of images and make the hif the group leader. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + None + + USAGE + Start script from script_manager + Assign keys to the shortcuts + + BUGS, COMMENTS, SUGGESTIONS + Bill Ferguson + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "hif_group_leader" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("7.0.0", MODULE) + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- 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.metadata = { + 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" +} + +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 + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- P R E F E R E N C E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.preferences.register(MODULE, "on_import", "bool", _("make hif group leader on import"), _("automatically make the hif file the group leader when raw + hif are imported"), true) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local jgloi = {} +jgloi.images = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function toggle_global_toolbox_grouping() + dt.gui.libs.global_toolbox.grouping = false + dt.gui.libs.global_toolbox.grouping = true +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N P R O G R A M +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function make_hif_group_leader(images) + -- if the image is part of a group, make it the leader + for _, image in ipairs(images) do + if #image:get_group_members() > 1 then + image:make_group_leader() + end + end + if dt.gui.libs.global_toolbox.grouping then + -- toggle the grouping to make the new leader show + toggle_global_toolbox_grouping() + end +end + +local function make_existing_hif_group_leader(images) + for _, image in ipairs(images) do + if string.lower(df.get_filetype(image.filename)) == "hif" then + if #image:get_group_members() > 1 then + image:make_group_leader() + end + end + end + if dt.gui.libs.global_toolbox.grouping then + -- toggle the grouping to make the new leader show + toggle_global_toolbox_grouping() + end +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + if dt.preferences.read(MODULE, "on_import", "bool") then + dt.destroy_event(MODULE, "post-import-film") + dt.destroy_event(MODULE, "post-import-image") + end + dt.destroy_event(MODULE .. "_collect", "shortcut") + dt.destroy_event(MODULE .. "_select", "shortcut") +end + +script_data.destroy = destroy + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +if dt.preferences.read(MODULE, "on_import", "bool") then + dt.register_event(MODULE, "post-import-film", + function(event, film_roll) + -- ignore the film roll, it contains all the images, not just the imported + local images = jgloi.images + if #images > 0 then + jgloi.images = {} + make_hif_group_leader(images) + end + end + ) + + dt.register_event(MODULE, "post-import-image", + function(event, image) + if string.lower(df.get_filetype(image.filename)) == "hif" then + table.insert(jgloi.images, image) + end + end + ) +end + +dt.register_event(MODULE .. "_collect", "shortcut", + function(event, shortcut) + -- ignore the film roll, it contains all the images, not just the imported + local images = dt.collection + make_existing_hif_group_leader(images) + end, + _("make hif group leader for collection") +) + +dt.register_event(MODULE .. "_select", "shortcut", + function(event, shortcut) + local images = dt.gui.selection() + make_existing_hif_group_leader(images) + end, + _("make hif group leader for selection") +) + +return script_data \ No newline at end of file diff --git a/contrib/hugin.lua b/contrib/hugin.lua index ed040f1e..46fbd90c 100644 --- a/contrib/hugin.lua +++ b/contrib/hugin.lua @@ -40,7 +40,7 @@ local du = require "lib/dtutils" local df = require "lib/dtutils.file" local log = require "lib/dtutils.log" local dtsys = require "lib/dtutils.system" -local gettext = dt.gettext +local gettext = dt.gettext.gettext local namespace = 'module_hugin' local user_pref_str = 'prefer_gui' @@ -56,20 +56,25 @@ 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") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + name = _("hugin"), + purpose = _("stitch images into a panorama"), + author = "Wolfgang Goetz", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/hugin" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("hugin",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("hugin", msgid) -end +script_data.show = nil -- only required for libs since the destroy_method only hides them local function user_preference_changed(widget) user_prefer_gui = widget.value @@ -78,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 @@ -139,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 @@ -200,7 +205,7 @@ local function create_panorama(storage, image_table, extra_data) --finalize if df.check_if_file_exists(src_path) then log.msg(log.debug, "found ", src_path, " importing to ", dst_path) df.file_move(src_path, dst_path) - dt.print(_("importing file "..dst_path)) + dt.print(string.format(_("importing file %s"), dst_path)) dt.database.import(dst_path) end end @@ -226,7 +231,7 @@ hugin_widget = dt.new_widget("box") { orientation = "vertical", dt.new_widget("check_button") { - label = _(" launch hugin gui"), + label = _("launch hugin gui"), value = user_prefer_gui, tooltip = _('launch hugin in gui mode'), clicked_callback = user_preference_changed diff --git a/contrib/image_stack.lua b/contrib/image_stack.lua index 8560c131..661b28d3 100644 --- a/contrib/image_stack.lua +++ b/contrib/image_stack.lua @@ -64,7 +64,7 @@ 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 +local gettext = dt.gettext.gettext local job = nil -- path separator constant @@ -73,20 +73,25 @@ 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") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("image_stack",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("image_stack", msgid) -end +script_data.show = nil -- only required for libs since the destroy_method only hides them -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- GUI definitions @@ -134,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"){ @@ -258,7 +263,7 @@ end -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - local function show_status(storage, image, format, filename, number, total, high_quality, extra_data) - dt.print(string.format(_("Export Image %i/%i"), number, total)) + dt.print(string.format(_("export image %i/%i"), number, total)) end -- read the gui and populate the align_image_stack arguments @@ -451,7 +456,7 @@ local function copy_image_attributes(from, to, ...) to.rights = from.rights to.description = from.description else - dt.print_error(_("Unrecognized option to copy_image_attributes: " .. arg)) + dt.print_error("Unrecognized option to copy_image_attributes: " .. arg) end end end @@ -495,11 +500,11 @@ local function image_stack(storage, image_table, extra_data) if image_count < 2 then dt.print(_("ERROR: at least 2 images required for image stacking, exiting...")) - dt.print_error(_(image_count .. " image(s) selected, at least 2 required")) + dt.print_error(image_count .. " image(s) selected, at least 2 required") 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 @@ -519,13 +524,13 @@ local function image_stack(storage, image_table, extra_data) job.percent = job.percent + percent_step else dt.print(_("ERROR: image alignment failed")) - dt.print_error(_("image alignment failed")) + dt.print_error("image alignment failed") cleanup(img_list) return end else dt.print(_("ERROR: align_image_stack not found")) - dt.print_error(_("align_image_stack not found")) + dt.print_error("align_image_stack not found") cleanup(img_list) return end @@ -539,7 +544,7 @@ local function image_stack(storage, image_table, extra_data) local ignore_tif_tags = " -quiet -define tiff:ignore-tags=40965,42032,42033,42034,42036,18246,18249,36867,34864,34866 " if convert_executable then local convert_command = convert_executable .. ignore_tif_tags .. convert_arguments - dt.print_log(_("convert command is " .. convert_command)) + dt.print_log("convert command is " .. convert_command) dt.print(_("processing image stack")) local result = dtsys.external_command(convert_command) if result == 0 then @@ -553,7 +558,7 @@ local function image_stack(storage, image_table, extra_data) local import_filename = df.create_unique_filename(film_roll_path .. PS .. df.get_filename(output_filename)) df.file_move(output_filename, import_filename) imported_image = dt.database.import(import_filename) - local created_tag = dt.tags.create(_("Created with|image_stack")) + local created_tag = dt.tags.create(_("created with|image_stack")) dt.tags.attach(created_tag, imported_image) -- all the images are the same except for time, so just copy the attributes -- from the first @@ -567,7 +572,7 @@ local function image_stack(storage, image_table, extra_data) if tag_source then dt.print(_("tagging source images")) - local source_tag = dt.tags.create(_("Source file|" .. imported_image.filename)) + local source_tag = dt.tags.create(_("source file|" .. imported_image.filename)) for img, _ in pairs(image_table) do dt.tags.attach(source_tag, img) end @@ -579,7 +584,7 @@ local function image_stack(storage, image_table, extra_data) end else dt.print(_("ERROR: convert executable not found")) - dt.print_error(_("convert executable not found")) + dt.print_error("convert executable not found") cleanup(img_list) end job.valid = false @@ -591,7 +596,7 @@ end dt.preferences.register("align_image_stack", "align_use_gpu", -- name "bool", -- type - _('align image stack: use GPU for remaping'), -- label + _('align image stack: use GPU for remapping'), -- label _('set the GPU remapping for image align'), -- tooltip false) diff --git a/contrib/image_time.lua b/contrib/image_time.lua index 78b6f42e..c0971306 100644 --- a/contrib/image_time.lua +++ b/contrib/image_time.lua @@ -107,7 +107,8 @@ local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" local ds = require "lib/dtutils.string" -local gettext = dt.gettext +local dtsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext local img_time = {} img_time.module_installed = false @@ -115,21 +116,27 @@ img_time.event_registered = false du.check_min_api_version("7.0.0", "image_time") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("image_time",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("image_time", msgid) -end local PS = dt.configuration.runnin_os == "windows" and "\\" or "/" local ERROR = -1 @@ -147,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 @@ -174,7 +181,7 @@ local function calculate_difference(images) img_time.diff_entry.text = calc_time_difference(images[1], images[2]) img_time.btn.sensitive = true else - dt.print(_("Error: 2 images must be selected")) + dt.print(_("ERROR: 2 images must be selected")) end end @@ -186,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) @@ -248,7 +255,7 @@ local function _get_windows_image_file_creation_time(image) end p:close() else - dt.print(_("unable to get information for ") .. image.filename) + dt.print(string.format(_("unable to get information for %s"), image.filename)) datetime = ERROR end return datetime @@ -265,7 +272,7 @@ local function _get_nix_image_file_creation_time(image) end p:close() else - dt.print(_("unable to get information for ") .. image.filename) + dt.print(string.format(_("unable to get information for %s"), image.filename)) datetime = ERROR end return datetime @@ -313,7 +320,7 @@ local function reset_time(images) image.exif_datetime_taken = get_original_image_time(image) end else - dt.print_error(_("reset time: no images selected")) + dt.print_error("reset time: no images selected") dt.print(_("please select the images that need their time reset")) end end @@ -429,8 +436,8 @@ end img_time.widgets = { -- name, type, tooltip, placeholder, {"ayr", "combobox", _("years"), _("years to adjust by, 0 - ?"), {seq(0,20)}, 1}, - {"amo", "combobox", _("months"), ("months to adjust by, 0-12"), {seq(0,12)}, 1}, - {"ady", "combobox", _("days"), ("days to adjust by, 0-31"), {seq(0,31)}, 1}, + {"amo", "combobox", _("months"), _("months to adjust by, 0-12"), {seq(0,12)}, 1}, + {"ady", "combobox", _("days"), _("days to adjust by, 0-31"), {seq(0,31)}, 1}, {"ahr", "combobox", _("hours"), _("hours to adjust by, 0-23"), {seq(0,23)}, 1}, {"amn", "combobox", _("minutes"), _("minutes to adjust by, 0-59"), {seq(0,59)}, 1}, {"asc", "combobox", _("seconds"), _("seconds to adjust by, 0-59"), {seq(0,59)}, 1}, @@ -456,13 +463,13 @@ end img_time.syr.selected = #img_time.syr img_time.diff_entry = dt.new_widget("entry"){ - tooltip = _("Time difference between images in seconds"), - placeholder = _("Select 2 images and use the calculate button"), + tooltip = _("time difference between images in seconds"), + placeholder = _("select 2 images and use the calculate button"), text = "", } img_time.calc_btn = dt.new_widget("button"){ - label = _("Calculate"), + label = _("calculate"), tooltip = _("calculate time difference between 2 images"), clicked_callback = function() calculate_difference(dt.gui.action_images) @@ -502,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 new file mode 100644 index 00000000..733071ed --- /dev/null +++ b/contrib/jpg_group_leader.lua @@ -0,0 +1,200 @@ +--[[ + + jpg_group_leader.lua - Make jpg image group leader + + 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 . +]] +--[[ + jpg_group_leader - Make jpg image group leader + + After a film roll is imported, check for RAW-JPG image groups + and make the JPG image the group leader. This is on by default + but can be disabled in preferences. + + Shortcuts are included to filter existing collections or + selections of images and make the jpg the group leader. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + None + + USAGE + Start script from script_manager + Assign keys to the shortcuts + + BUGS, COMMENTS, SUGGESTIONS + Bill Ferguson + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "jpg_group_leader" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("7.0.0", MODULE) + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- 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.metadata = { + 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" +} + +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 + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- P R E F E R E N C E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.preferences.register(MODULE, "on_import", "bool", _("make jpg group leader on import"), _("automatically make the jpg file the group leader when raw + jpg are imported"), true) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local jgloi = {} +jgloi.images = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function toggle_global_toolbox_grouping() + dt.gui.libs.global_toolbox.grouping = false + dt.gui.libs.global_toolbox.grouping = true +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N P R O G R A M +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function make_jpg_group_leader(images) + -- if the image is part of a group, make it the leader + for _, image in ipairs(images) do + if #image:get_group_members() > 1 then + image:make_group_leader() + end + end + if dt.gui.libs.global_toolbox.grouping then + -- toggle the grouping to make the new leader show + toggle_global_toolbox_grouping() + end +end + +local function make_existing_jpg_group_leader(images) + for _, image in ipairs(images) do + if string.lower(df.get_filetype(image.filename)) == "jpg" then + if #image:get_group_members() > 1 then + image:make_group_leader() + end + end + end + if dt.gui.libs.global_toolbox.grouping then + -- toggle the grouping to make the new leader show + toggle_global_toolbox_grouping() + end +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + if dt.preferences.read(MODULE, "on_import", "bool") then + dt.destroy_event(MODULE, "post-import-film") + dt.destroy_event(MODULE, "post-import-image") + end + dt.destroy_event(MODULE .. "_collect", "shortcut") + dt.destroy_event(MODULE .. "_select", "shortcut") +end + +script_data.destroy = destroy + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +if dt.preferences.read(MODULE, "on_import", "bool") then + dt.register_event(MODULE, "post-import-film", + function(event, film_roll) + -- ignore the film roll, it contains all the images, not just the imported + local images = jgloi.images + if #images > 0 then + jgloi.images = {} + make_jpg_group_leader(images) + end + end + ) + + dt.register_event(MODULE, "post-import-image", + function(event, image) + if string.lower(df.get_filetype(image.filename)) == "jpg" then + table.insert(jgloi.images, image) + end + end + ) +end + +dt.register_event(MODULE .. "_collect", "shortcut", + function(event, shortcut) + -- ignore the film roll, it contains all the images, not just the imported + local images = dt.collection + make_existing_jpg_group_leader(images) + end, + _("make jpg group leader for collection") +) + +dt.register_event(MODULE .. "_select", "shortcut", + function(event, shortcut) + local images = dt.gui.selection() + make_existing_jpg_group_leader(images) + end, + _("make jpg group leader for selection") +) + +return script_data \ No newline at end of file diff --git a/contrib/kml_export.lua b/contrib/kml_export.lua index 8451d72c..561724fa 100644 --- a/contrib/kml_export.lua +++ b/contrib/kml_export.lua @@ -39,29 +39,34 @@ local df = require "lib/dtutils.file" local ds = require "lib/dtutils.string" local dsys = require "lib/dtutils.system" -local gettext = dt.gettext +local gettext = dt.gettext.gettext local PS = dt.configuration.running_os == "windows" and "\\" or "/" du.check_min_api_version("7.0.0", "kml_export") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("kml_export",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("kml_export", msgid) -end +script_data.show = nil -- only required for libs since the destroy_method only hides them local function show_status(storage, image, format, filename, number, total, high_quality, extra_data) - dt.print(string.format(_("Export Image %i/%i"), number, total)) + dt.print(string.format(_("export image %i/%i"), number, total)) end -- Add duplicate index to filename @@ -88,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 @@ -103,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 @@ -319,14 +324,14 @@ if dt.configuration.running_os == "windows" then "OpenKmlFile", "bool", _("KML export: Open KML file after export"), - _("Opens the KML file after the export with the standard program for KML files"), + _("opens the KML file after the export with the standard program for KML files"), false ) else dt.preferences.register("kml_export", "OpenKmlFile", "bool", - _("KML export: Open KML/KMZ file after export"), - _("Opens the KML file after the export with the standard program for KML files"), + _("KML export: open KML/KMZ file after export"), + _("opens the KML file after the export with the standard program for KML files"), false ) end @@ -345,23 +350,23 @@ end dt.preferences.register("kml_export", "ExportDirectory", "directory", - _("KML export: Export directory"), - _("A directory that will be used to export the KML/KMZ files"), + _("KML export: export directory"), + _("a directory that will be used to export the KML/KMZ files"), defaultDir ) if dt.configuration.running_os ~= "linux" then dt.preferences.register("kml_export", "magickPath", -- name "file", -- type - _("KML export: ImageMagick binary Location"), -- label - _("Install location of magick[.exe]. Requires restart to take effect."), -- tooltip + _("KML export: ImageMagick binary location"), -- label + _("install location of magick[.exe], requires restart to take effect"), -- tooltip "magick") -- default end dt.preferences.register("kml_export", "CreatePath", "bool", - _("KML export: Connect images with path"), + _("KML export: connect images with path"), _("connect all images with a path"), false ) @@ -369,16 +374,16 @@ if dt.configuration.running_os == "linux" then dt.preferences.register("kml_export", "CreateKMZ", "bool", - _("KML export: Create KMZ file"), - _("Compress all imeges to one KMZ file"), + _("KML export: create KMZ file"), + _("compress all imeges to one KMZ file"), true ) end -- Register if dt.configuration.running_os == "windows" then - dt.register_storage("kml_export", _("KML Export"), nil, create_kml_file) + dt.register_storage("kml_export", _("KML export"), nil, create_kml_file) else - dt.register_storage("kml_export", _("KML/KMZ Export"), nil, create_kml_file) + dt.register_storage("kml_export", _("KML/KMZ export"), nil, create_kml_file) end script_data.destroy = destroy diff --git a/contrib/passport_guide.lua b/contrib/passport_guide.lua index 0b617286..34e9fc79 100644 --- a/contrib/passport_guide.lua +++ b/contrib/passport_guide.lua @@ -37,17 +37,28 @@ 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") --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("passport_guide",dt.configuration.config_dir.."/lua/locale/") - local function _(msgid) - return gettext.dgettext("passport_guide", msgid) + return gettext(msgid) end +local script_data = {} + +script_data.metadata = { + 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" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + dt.guides.register_guide("passport", -- draw function(cairo, x, y, width, height, zoom_scale) @@ -91,5 +102,13 @@ function() end ) +local function destroy() + -- nothing to destroy +end + +script_data.destroy = destroy + +return script_data + -- kate: tab-indents: off; indent-width 2; replace-tabs on; remove-trailing-space on; hl Lua; -- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua diff --git a/contrib/passport_guide_germany.lua b/contrib/passport_guide_germany.lua new file mode 100644 index 00000000..a1b9f618 --- /dev/null +++ b/contrib/passport_guide_germany.lua @@ -0,0 +1,125 @@ +--[[ + German passport photo cropping guide for darktable. + Derived from the passport cropping guide by Kåre Hampf. + + copyright (c) 2017 Kåre Hampf + copyright (c) 2024 Christian Sültrop + + 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 . +]] + +--[[ +PASSPORT CROPPING GUIDE +Guides for cropping passport and ID card ("Personalausweis") photos based on the "Passbild-Schablone" +from the German Federal Ministry of the Interior and Community. +(https://www.bmi.bund.de/SharedDocs/downloads/DE/veroeffentlichungen/themen/moderne-verwaltung/ausweise/passbild-schablone-erwachsene.pdf?__blob=publicationFile&v=3) + +INSTALLATION +* copy this file in $CONFIGDIR/lua/ where CONFIGDIR is your darktable configuration directory +* add the following line in the file $CONFIGDIR/luarc + require "passport_guide_germany" +* (optional) add the line: + "plugins/darkroom/clipping/extra_aspect_ratios/passport 35x45mm=45:35" + to $CONFIGDIR/darktablerc + +USAGE +* when using the cropping tool, select "Passport Photo Germany" as guide and if you added the line in yout rc + select "passport 35x45mm" as aspect +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +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 +local function _(msgid) + return gettext(msgid) +end + +local script_data = {} + +script_data.metadata = { + 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" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +dt.guides.register_guide("Passport Photo Germany", +-- draw +function(cairo, x, y, width, height, zoom_scale) + local _width, _height + + -- get the max 35x45 rectangle + local aspect_ratio = 45 / 35 + if width * aspect_ratio > height then + _width = height / aspect_ratio + _height = height + else + _width = width + _height = width * aspect_ratio + end + + cairo:save() + + cairo:translate(x + (width - _width) / 2, y + (height - _height) / 2) + cairo:scale(_width / 35, _height / 45) + + -- the outer rectangle + cairo:rectangle( 0, 0, 35, 45) + + -- Nose position: The nose tip must be between these lines + cairo:draw_line(15.5, 45, 15.5, 13) + cairo:draw_line(35-15.5, 45, 35-15.5, 13) + + -- Face height + -- optimum face height: The upper end of the head should be between these lines + cairo:draw_line(0, 4, 35, 4) + cairo:draw_line(0, 8, 35, 8) + + -- tolerated face height: The upper end of the head must not be below this line + cairo:draw_line(6, 13, 30, 13) + + -- Eye area: The eyes must be between these lines + cairo:draw_line(0, 13, 35, 13) + cairo:draw_line(0, 23, 35, 23) + + -- Cheek line: The cheek must lie on this line + cairo:draw_line(9, 45-5, 27, 45-5) + + cairo:restore() +end, +-- gui +function() + return dt.new_widget("label"){label = _("Passport Photo Germany"), halign = "start"} +end +) + +local function destroy() + -- noting to destroy +end + +script_data.destroy = destroy + +return script_data + +-- kate: tab-indents: off; indent-width 2; replace-tabs on; remove-trailing-space on; hl Lua; +-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua diff --git a/contrib/pdf_slideshow.lua b/contrib/pdf_slideshow.lua index f1824856..b93ae978 100644 --- a/contrib/pdf_slideshow.lua +++ b/contrib/pdf_slideshow.lua @@ -42,17 +42,14 @@ local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" -local gettext = dt.gettext - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("pdf_slideshow",dt.configuration.config_dir.."/lua/locale/") +local gettext = dt.gettext.gettext local function _(msgid) - return gettext.dgettext("pdf_slideshow", msgid) + return gettext(msgid) end if not df.check_if_bin_exists("pdflatex") then - dt.print_error(_("pdflatex not found")) + dt.print_error("pdflatex not found") return end @@ -62,9 +59,17 @@ du.check_min_api_version("7.0.0", "pdf_slideshow") local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them dt.preferences.register ("pdf_slideshow","open with","string", @@ -230,7 +235,7 @@ dt.register_storage("pdf_slideshow",_("pdf slideshow"), local result = dt.control.execute(command) if result ~= 0 then dt.print(_("problem running pdflatex")) -- this one is probably usefull to the user - error(_("problem running ")..command) + error("problem running "..command) end -- open the PDF @@ -240,7 +245,7 @@ dt.register_storage("pdf_slideshow",_("pdf slideshow"), local result = dt.control.execute(command) if result ~= 0 then dt.print(_("problem running pdf viewer")) -- this one is probably usefull to the user - error(_("problem running ")..command) + error("problem running "..command) end -- finally do some clean-up diff --git a/contrib/photils.lua b/contrib/photils.lua index 3be939e2..7bb39c6f 100644 --- a/contrib/photils.lua +++ b/contrib/photils.lua @@ -44,19 +44,30 @@ local dtsys = require "lib/dtutils.system" local MODULE_NAME = "photils" du.check_min_api_version("7.0.0", MODULE_NAME) +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them local PS = dt.configuration.running_os == "windows" and "\\" or "/" -local gettext = dt.gettext -gettext.bindtextdomain(MODULE_NAME, - dt.configuration.config_dir .. PS .. "lua" .. PS .. "locale" .. PS) local exporter = dt.new_format("jpeg") exporter.quality = 80 @@ -65,10 +76,6 @@ exporter.max_width = 224 -- helper functions -local function _(msgid) - return gettext.dgettext(MODULE_NAME, msgid) -end - local function num_keys(tbl) local num = 0 for _ in pairs(tbl) do num = num + 1 end @@ -177,7 +184,7 @@ function PHOTILS.image_changed() end function PHOTILS.tagged_image_has_changed() - GUI.warning.label = _("The suggested tags were not generated\n for the currently selected image!") + GUI.warning.label = _("the suggested tags were not generated\n for the currently selected image!") end function PHOTILS.paginate() @@ -234,7 +241,7 @@ end function PHOTILS.attach_tags() local num_selected = #dt.gui.selection() - local job = dt.gui.create_job(_("Apply tag to image"), true) + local job = dt.gui.create_job(_("apply tag to image"), true) for i = 1, num_selected, 1 do local image = dt.gui.selection()[i] @@ -246,7 +253,7 @@ function PHOTILS.attach_tags() job.percent = i / num_selected end - dt.print(_("Tags successfully attached to image")) + dt.print(_("tags successfully attached to image")) job.valid = false end @@ -312,11 +319,11 @@ function PHOTILS.on_tags_clicked() local images = dt.gui.selection() if #images == 0 then - dt.print(_("No image selected.")) + dt.print(_("no image selected.")) dt.control.sleep(2000) else if #images > 1 then - dt.print(_("This plugin can only handle a single image.")) + dt.print(_("this plugin can only handle a single image.")) dt.gui.selection({images[1]}) dt.control.sleep(2000) end @@ -331,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 @@ -386,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, @@ -427,9 +434,9 @@ end if not photils_installed then GUI.warning_label.label = _("photils-cli not found") - dt.print_log(_("photils-cli not found")) + dt.print_log("photils-cli not found") else - GUI.warning_label.label = _("Select an image, click \"get tags\" and get \nsuggestions for tags.") + GUI.warning_label.label = _("select an image, click \"get tags\" and get \nsuggestions for tags.") end GUI.pagination = dt.new_widget("box") { @@ -472,9 +479,9 @@ dt.preferences.register(MODULE_NAME, "export_image_before_for_tags", "bool", _("photils: use exported image for tag request"), - _("If enabled, the image passed to photils for tag suggestion is based on the exported, already edited image. " .. - "Otherwise, the embedded thumbnail of the RAW file will be used for tag suggestion." .. - "The embedded thumbnail could speedup the tag suggestion but can fail if the RAW file is not supported."), + _("if enabled, the image passed to photils for tag suggestion is based on the exported, already edited image. " .. + "otherwise, the embedded thumbnail of the RAW file will be used for tag suggestion." .. + "the embedded thumbnail could speedup the tag suggestion but can fail if the RAW file is not supported."), true) dt.register_event("photils", "mouse-over-image-changed", diff --git a/contrib/quicktag.lua b/contrib/quicktag.lua index 15f1d8db..c218f09c 100644 --- a/contrib/quicktag.lua +++ b/contrib/quicktag.lua @@ -49,10 +49,23 @@ local debug = require "darktable.debug" du.check_min_api_version("7.0.0", "quicktag") +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again @@ -62,15 +75,6 @@ qt.module_installed = false qt.event_registered = false qt.widget_table = {} -local gettext = dt.gettext - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("quicktag",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("quicktag", msgid) -end - -- maximum length of button labels dt.preferences.register("quickTag", "labellength", @@ -193,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}}, @@ -300,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 4f2fa608..0e9ddb29 100644 --- a/contrib/rate_group.lua +++ b/contrib/rate_group.lua @@ -44,13 +44,27 @@ local du = require "lib/dtutils" -- added version check du.check_min_api_version("7.0.0", "rate_group") +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them local function apply_rating(rating) local images = dt.gui.action_images @@ -61,9 +75,9 @@ local function apply_rating(rating) end end if rating < 0 then - dt.print("rejecting group(s)") + dt.print(_("rejecting group(s)")) else - dt.print("applying rating " ..rating.. " to group(s)") + dt.print(string.format(_("applying rating %d to group(s)"), rating)) end end @@ -80,37 +94,38 @@ end dt.register_event("rg_reject", "shortcut", function(event, shortcut) apply_rating(-1) -end, "Reject group") +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 74d9bfed..ae77056a 100644 --- a/contrib/rename-tags.lua +++ b/contrib/rename-tags.lua @@ -36,21 +36,35 @@ local debug = require "darktable.debug" du.check_min_api_version("7.0.0", "rename-tags") du.deprecated("contrib/rename-tags.lua","darktable release 4.0") +local gettext = darktable.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them local rt = {} rt.module_installed = false rt.event_registered = false -- GUI entries -local old_tag = darktable.new_widget("entry") { tooltip = "Enter old tag" } -local new_tag = darktable.new_widget("entry") { tooltip = "Enter new tag" } +local old_tag = darktable.new_widget("entry") { tooltip = _("enter old tag") } +local new_tag = darktable.new_widget("entry") { tooltip = _("enter new tag") } local function rename_reset() old_tag.text = '' @@ -61,11 +75,11 @@ end local function rename_tags() -- If entries are empty, return if old_tag.text == '' then - darktable.print ("Old tag can't be empty") + darktable.print (_("old tag can't be empty")) return end if new_tag.text == '' then - darktable.print ("New tag can't be empty") + darktable.print (_("new tag can't be empty")) return end @@ -75,12 +89,12 @@ local function rename_tags() local ot = darktable.tags.find (old_tag.text) if not ot then - darktable.print ("Old tag does not exist") + darktable.print (_("old tag does not exist")) return end -- Show job - local job = darktable.gui.create_job ("Renaming tag", true) + local job = darktable.gui.create_job (_("renaming tag"), true) old_tag.editable = false new_tag.editable = false @@ -103,7 +117,7 @@ local function rename_tags() darktable.tags.delete (ot) job.valid = false - darktable.print ("Renamed tags for " .. count .. " images") + darktable.print (string.format(_("renamed tags for %d images"), count)) old_tag.editable = true new_tag.editable = true @@ -114,7 +128,7 @@ end local function install_module() if not rt.module_installed then - darktable.register_lib ("rename_tags", "rename tag", true, true, {[darktable.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 20},}, rt.rename_widget, nil, nil) + darktable.register_lib ("rename_tags", _("rename tag"), true, true, {[darktable.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 20},}, rt.rename_widget, nil, nil) rt.module_installed = true end end @@ -130,13 +144,13 @@ end -- GUI local old_widget = darktable.new_widget ("box") { orientation = "horizontal", - darktable.new_widget("label") { label = "Old tag" }, + darktable.new_widget("label") { label = _("old tag") }, old_tag } local new_widget = darktable.new_widget ("box") { orientation = "horizontal", - darktable.new_widget("label") { label = "New tag" }, + darktable.new_widget("label") { label = _("new tag") }, new_tag } @@ -145,7 +159,7 @@ rt.rename_widget = darktable.new_widget ("box") { reset_callback = rename_reset, old_widget, new_widget, - darktable.new_widget("button") { label = "Go", clicked_callback = rename_tags } + darktable.new_widget("button") { label = _("go"), clicked_callback = rename_tags } } if darktable.gui.current_view().id == "lighttable" then diff --git a/contrib/rename_images.lua b/contrib/rename_images.lua index 584ba31a..0093c540 100644 --- a/contrib/rename_images.lua +++ b/contrib/rename_images.lua @@ -43,27 +43,19 @@ 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 - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("rename_images",dt.configuration.config_dir.."/lua/locale/") +local gettext = dt.gettext.gettext local function _(msgid) - return gettext.dgettext("rename_images", msgid) + return gettext(msgid) 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,9 +64,17 @@ rename.event_registered = false -- script_manager integration local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them -- - - - - - - - - - - - - - - - - - - - - - - - @@ -85,90 +85,13 @@ local MODULE_NAME = "rename_images" local PS = dt.configuration.running_os == "windows" and "\\" or "/" local USER = os.getenv("USERNAME") local HOME = os.getenv(dt.configuration.running_os == "windows" and "HOMEPATH" or "HOME") -local PICTURES = HOME .. PS .. dt.configuration.running_os == "windows" and "My Pictures" or "Pictures" +local PICTURES = HOME .. PS .. dt.configuration.running_os == "windows" and _("My Pictures") or _("Pictures") 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(_("unknown variable " .. var .. ", aborting...")) - 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 @@ -207,9 +130,10 @@ 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)) + dt.print_log("pattern is " .. pattern) if string.len(pattern) > 0 then local datetime = os.date("*t") @@ -217,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 @@ -249,7 +173,8 @@ local function do_rename(images) stop_job(job) local collect_rules = dt.gui.libs.collect.filter() dt.gui.libs.collect.filter(collect_rules) - dt.print(_("renamed " .. #images .. " images")) + 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...") dt.print(_("please enter the new name or pattern")) @@ -269,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 new file mode 100644 index 00000000..b47c7cf6 --- /dev/null +++ b/contrib/select_non_existing.lua @@ -0,0 +1,84 @@ +--[[ + This file is part of darktable, + copyright (c) 2023 Dirk Dittmar + + 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 . +]] +--[[ +Enable selection of non-existing images in the the currently worked on images, e.g. the ones selected by the collection module. +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" + +-- module name +local MODULE = "select_non_existing" + +du.check_min_api_version("9.1.0", MODULE) + +-- figure out the path separator +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +local function stop_job(job) + job.valid = false +end + +local function select_nonexisting_images(event, images) + local selection = {} + + local job = dt.gui.create_job(_("select non existing images"), true, stop_job) + for key,image in ipairs(images) do + if(job.valid) then + job.percent = (key - 1)/#images + local filepath = image.path..PS..image.filename + local file_exists = df.test_file(filepath, "e") + dt.print_log(filepath.." exists? => "..tostring(file_exists)) + if (not file_exists) then + table.insert(selection, image) + end + else + break + end + end + stop_job(job) + + return selection +end + +local function destroy() + dt.gui.libs.select.destroy_selection(MODULE) +end + +dt.gui.libs.select.register_selection( + MODULE, + _("select non existing"), + select_nonexisting_images, + _("select all non-existing images in the current images")) + +local script_data = {} + +script_data.metadata = { + 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" +} + +script_data.destroy = destroy +return script_data \ No newline at end of file diff --git a/contrib/select_untagged.lua b/contrib/select_untagged.lua index 8740ee2f..0ca4b5d2 100644 --- a/contrib/select_untagged.lua +++ b/contrib/select_untagged.lua @@ -20,24 +20,29 @@ Enable selection of untagged images (darktable|* tags are ignored) local dt = require "darktable" local du = require "lib/dtutils" -local gettext = dt.gettext +local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "select_untagged") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("select_untagged",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("select_untagged", msgid) -end +script_data.show = nil -- only required for libs since the destroy_method only hides them local function stop_job(job) job.valid = false diff --git a/contrib/slideshowMusic.lua b/contrib/slideshowMusic.lua index 67c17363..bed3da6f 100644 --- a/contrib/slideshowMusic.lua +++ b/contrib/slideshowMusic.lua @@ -27,24 +27,29 @@ USAGE local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" -local gettext = dt.gettext +local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "slideshowMusic") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("slideshowMusic",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("slideshowMusic", msgid) -end +script_data.show = nil -- only required for libs since the destroy_method only hides them local function playSlideshowMusic(_, old_view, new_view) local filename, playMusic @@ -53,7 +58,7 @@ local function playSlideshowMusic(_, old_view, new_view) playMusic = dt.preferences.read("slideshowMusic","PlaySlideshowMusic","bool") if not df.check_if_bin_exists("rhythmbox-client") then - dt.print_error(_("rhythmbox-client not found")) + dt.print_error("rhythmbox-client not found") return end @@ -69,7 +74,7 @@ local function playSlideshowMusic(_, old_view, new_view) if (old_view and old_view.id == "slideshow") then stopCommand = "rhythmbox-client --pause" --dt.print_error(stopCommand) - dt.control.execute( stopCommand) + dt.control.execute(stopCommand) end end end @@ -82,12 +87,12 @@ function destroy() end -- Preferences -dt.preferences.register("slideshowMusic", "SlideshowMusic", "file", _("Slideshow background music file"), "", "") +dt.preferences.register("slideshowMusic", "SlideshowMusic", "file", _("slideshow background music file"), "", "") dt.preferences.register("slideshowMusic", "PlaySlideshowMusic", "bool", - _("Play slideshow background music"), - _("Plays music with rhythmbox if a slideshow starts"), + _("play slideshow background music"), + _("plays music with rhythmbox if a slideshow starts"), true) -- Register dt.register_event("slideshow_music", "view-changed", diff --git a/contrib/transfer_hierarchy.lua b/contrib/transfer_hierarchy.lua index 840deeca..dd9c2708 100755 --- a/contrib/transfer_hierarchy.lua +++ b/contrib/transfer_hierarchy.lua @@ -76,17 +76,30 @@ 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) +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them local MKDIR_COMMAND = darktable.configuration.running_os == "windows" and "mkdir " or "mkdir -p " local PATH_SEPARATOR = darktable.configuration.running_os == "windows" and "\\\\" or "/" @@ -95,12 +108,6 @@ local PATH_SEGMENT_REGEX = "(" .. PATH_SEPARATOR .. "?)([^" .. PATH_SEPARATOR .. unpack = unpack or table.unpack gmatch = string.gfind or string.gmatch -darktable.gettext.bindtextdomain(LIB_ID, darktable.configuration.config_dir .. PATH_SEPARATOR .. "lua" .. PATH_SEPARATOR .. "locale" .. PATH_SEPARATOR) - -local function _(msgid) - return darktable.gettext.dgettext(LIB_ID, msgid) -end - -- Header material: END @@ -137,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 @@ -150,7 +157,7 @@ end -- Widgets and business logic: BEGIN local sourceTextBox = darktable.new_widget("entry") { - tooltip = _("Lowest directory containing all selected images"), + tooltip = _("lowest directory containing all selected images"), editable = false } sourceTextBox.reset_callback = function() sourceTextBox.text = "" end @@ -267,17 +274,17 @@ local function doTransfer(transferFunc) films[film] = destBase .. string.sub(film.path, #sourceBase+1) if not pathExists(films[film]) then if createDirectory(films[film]) == nil then - darktable.print(_("transfer hierarchy: ERROR: could not create directory: " .. films[film])) + darktable.print(string.format(_("transfer hierarchy: ERROR: could not create directory: %s"), films[film])) return end end if not pathIsDirectory(films[film]) then - darktable.print(_("transfer hierarchy: ERROR: not a directory: " .. films[film])) + darktable.print(string.format(_("transfer hierarchy: ERROR: not a directory: %s"), films[film])) return end destFilms[film] = darktable.films.new(films[film]) if destFilms[film] == nil then - darktable.print(_("transfer hierarchy: ERROR: could not create film: " .. film.path)) + darktable.print(string.format(_("transfer hierarchy: ERROR: could not create film: %s"), film.path)) end end @@ -286,7 +293,7 @@ local function doTransfer(transferFunc) srcFilms[img] = img.film end - local job = darktable.gui.create_job(string.format(_("transfer hierarchy") .. " (%d image" .. (#(darktable.gui.action_images) == 1 and "" or "s") .. ")", #(darktable.gui.action_images)), true, stopTransfer) + local job = darktable.gui.create_job(string.format(_("transfer hierarchy (%d image%s)"), #(darktable.gui.action_images), (#(darktable.gui.action_images) == 1 and "" or "s")), true, stopTransfer) job.percent = 0.0 local pctIncrement = 1.0 / #(darktable.gui.action_images) for _,img in ipairs(darktable.gui.action_images) do @@ -344,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 } } @@ -398,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 d670a517..f0713fa6 100644 --- a/contrib/video_ffmpeg.lua +++ b/contrib/video_ffmpeg.lua @@ -37,27 +37,33 @@ local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" local dsys = require "lib/dtutils.system" -local gettext = dt.gettext +local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "video_ffmpeg") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them local MODULE_NAME = "video_ffmpeg" local PS = dt.configuration.running_os == "windows" and "\\" or "/" -gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS) - -local function _(msgid) - return gettext.dgettext(MODULE_NAME, msgid) -end - ---- DECLARATIONS @@ -240,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) @@ -293,7 +299,7 @@ else end local output_directory_chooser = dt.new_widget("file_chooser_button"){ - title = _("Select export path"), + title = _("select export path"), is_directory = true, tooltip =_("select the target directory for the timelapse. \nthe filename is created automatically."), value = string_pref_read("export_path", defaultVideoDir), @@ -374,7 +380,7 @@ local module_widget = dt.new_widget("box") { ---- EXPORT & REGISTRATION local function show_status(enf_storage, image, format, filename, number, total, high_quality, extra_data) - dt.print(_("export ")..tostring(number).." / "..tostring(total)) + dt.print(string.format(_("export %d / %d", number), total)) end local function init_export(storage, img_format, images, high_quality, extra_data) @@ -410,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"] @@ -442,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")) @@ -464,7 +470,7 @@ end dt.register_storage( "module_video_ffmpeg", - _(MODULE_NAME), + _("video ffmpeg"), show_status, finalize_export, nil, 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 f2aaff72..bf8a1c17 100644 --- a/examples/api_version.lua +++ b/examples/api_version.lua @@ -23,6 +23,14 @@ USAGE ]] local dt = require "darktable" +-- translation facilities + +local gettext = dt.gettext.gettext + +local function _(msg) + return gettext(msg) +end + -- script_manager integration to allow a script to be removed -- without restarting darktable local function destroy() @@ -30,12 +38,21 @@ local function destroy() end local result = dt.configuration.api_version_string -dt.print_error("API Version: "..result) +dt.print_log("API Version: " .. 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 -- script_manager local script_data = {} + +script_data.metadata = { + name = _("APIversion"), + purpose = _("display api_version example"), + author = "Tobias Jakobs", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/api_version" +} + script_data.destroy = destroy return script_data diff --git a/examples/darkroom_demo.lua b/examples/darkroom_demo.lua index b4edcc56..b76b552e 100644 --- a/examples/darkroom_demo.lua +++ b/examples/darkroom_demo.lua @@ -40,7 +40,7 @@ local du = require "lib/dtutils" -- V E R S I O N C H E C K -- - - - - - - - - - - - - - - - - - - - - - - - -du.check_min_api_version("5.0.2", "darkroom_mode") -- darktable 3.0 +du.check_min_api_version("5.0.2", "darkroom_demo") -- darktable 3.0 -- script_manager integration to allow a script to be removed -- without restarting darktable @@ -52,17 +52,16 @@ end -- C O N S T A N T S -- - - - - - - - - - - - - - - - - - - - - - - - -local MODULE_NAME = "darkroom" +local MODULE_NAME = "darkroom_demo" local PS = dt.configuration.running_os == "windows" and "\\" or "/" -- - - - - - - - - - - - - - - - - - - - - - - - -- T R A N S L A T I O N S -- - - - - - - - - - - - - - - - - - - - - - - - -local gettext = dt.gettext -gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS) +local gettext = dt.gettext.gettext local function _(msgid) - return gettext.dgettext(MODULE_NAME, msgid) + return gettext(msgid) end -- - - - - - - - - - - - - - - - - - - - - - - - @@ -92,13 +91,13 @@ 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 for i, img in ipairs(dt.collection) do - dt.print(_("Displaying image " .. i)) + dt.print(string.format(_("displaying image "), i)) dt.gui.views.darkroom.display_image(img) sleep(1500) if i == max_images then @@ -108,7 +107,7 @@ end -- return to lighttable view -dt.print(_("Restoring view")) +dt.print(_("restoring view")) sleep(1500) dt.gui.current_view(current_view) @@ -116,6 +115,14 @@ dt.gui.current_view(current_view) -- it's time to destroy the script and then return the data to -- script_manager local script_data = {} + +script_data.metadata = { + 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" +} + script_data.destroy = destroy return script_data diff --git a/examples/gettextExample.lua b/examples/gettextExample.lua index 59a676c1..42e1a479 100644 --- a/examples/gettextExample.lua +++ b/examples/gettextExample.lua @@ -67,27 +67,31 @@ end -- Not translated Text dt.print_error("Hello World!") -local gettext = dt.gettext --- Translate a string using the darktable textdomain -dt.print_error(gettext.gettext("image")) - --- Tell gettext where to find the .mo file translating messages for a particular domain +local gettext = dt.gettext.gettext -gettext.bindtextdomain("gettextExample",dt.configuration.config_dir.."/lua/locale/") --- Translate a string using the specified textdomain -dt.print_error(gettext.dgettext("gettextExample", 'Hello World!')) +-- Translate a string using the darktable textdomain +dt.print_error(gettext("image")) -- Define a local function called _ to make the code more readable and have it call dgettext -- with the proper domain. local function _(msgid) - return gettext.dgettext("gettextExample", msgid) + return gettext(msgid) end -dt.print_error(_('Hello World!')) + +dt.print_error(_("hello world!")) -- 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 -- script_manager local script_data = {} + +script_data.metadata = { + 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" +} + script_data.destroy = destroy return script_data diff --git a/examples/gui_action.lua b/examples/gui_action.lua new file mode 100644 index 00000000..9fee3619 --- /dev/null +++ b/examples/gui_action.lua @@ -0,0 +1,142 @@ +local dt = require "darktable" + +local NaN = 0/0 + +local wg = {} + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + 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" +} + +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 + +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'") + } + +wg.instance = dt.new_widget("combobox"){ + 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") +} + +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") +} + +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") +} + +wg.check = dt.new_widget("check_button"){ + label = _('perform action'), + tooltip = _('perform action or only read return'), + clicked_callback = function() + wg.speed.sensitive = wg.check.value + end, + value = true +} + +wg.return_value = dt.new_widget("entry"){ + text = "", + sensitive = false +} + +dt.register_lib( + "execute_action", -- Module name + _("execute gui actions"), -- name + true, -- expandable + false, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 100}, + [dt.gui.views.darkroom] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 100}}, + dt.new_widget("box") + { + orientation = "vertical", + + dt.new_widget("box") + { + orientation = "horizontal", + 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"}, + wg.element + }, + dt.new_widget("box") + { + orientation = "horizontal", + 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"}, + wg.speed + }, + dt.new_widget("button") + { + 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, tonumber(wg.instance.value), wg.element.text, wg.effect.text, tonumber(sp)) + end + }, + dt.new_widget("box") + { + orientation = "horizontal", + dt.new_widget("label"){label = "return value:", halign = "start"}, + wg.return_value + }, + } + ) + +local function restart() + dt.gui.libs["execute_action"].visible = true +end + +local function destroy() + dt.gui.libs["execute_action"].visible = false +end + +script_data.destroy = destroy +script_data.destroy_method = "hide" +script_data.restart = restart +script_data.show = restart + +return script_data diff --git a/examples/hello_world.lua b/examples/hello_world.lua index f8fba8d4..6e04efc4 100644 --- a/examples/hello_world.lua +++ b/examples/hello_world.lua @@ -29,20 +29,36 @@ USAGE local dt = require "darktable" local du = require "lib/dtutils" +-- translation facilities + du.check_min_api_version("2.0.0", "hello_world") +local gettext = dt.gettext.gettext + +local function _(msg) + return gettext(msg) +end + -- script_manager integration to allow a script to be removed -- without restarting darktable local function destroy() -- nothing to destroy end -dt.print("hello, world") +dt.print(_("hello, world")) -- 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 -- script_manager local script_data = {} + +script_data.metadata = { + 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" +} + script_data.destroy = destroy return script_data diff --git a/examples/lighttable_demo.lua b/examples/lighttable_demo.lua index fc071325..e28454cd 100644 --- a/examples/lighttable_demo.lua +++ b/examples/lighttable_demo.lua @@ -59,11 +59,12 @@ end -- - - - - - - - - - - - - - - - - - - - - - - - -- T R A N S L A T I O N S -- - - - - - - - - - - - - - - - - - - - - - - - -local gettext = dt.gettext -gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS) +local gettext = dt.gettext.gettext + +dt.gettext.bindtextdomain("lighttable_demo", dt.configuration.config_dir .."/lua/locale/") local function _(msgid) - return gettext.dgettext(MODULE_NAME, msgid) + return gettext(msgid) end -- - - - - - - - - - - - - - - - - - - - - - - - @@ -145,17 +146,17 @@ sleep(2000) for n, layout in ipairs(layouts) do dt.gui.libs.lighttable_mode.layout(layout) - dt.print(_("set lighttable layout to " .. layout)) - dt.print_log(_("set lighttable layout to " .. layout)) + dt.print(string.format(_("set lighttable layout to %s"), layout)) + dt.print_log("set lighttable layout to " .. layout) sleep(1500) for i = 1, 10 do dt.gui.libs.lighttable_mode.zoom_level(i) - dt.print(_("Set zoom level to " .. i)) + dt.print(string.format(_("set zoom level to %d"), i)) sleep(1500) end for i = 9, 1, -1 do dt.gui.libs.lighttable_mode.zoom_level(i) - dt.print(_("Set zoom level to " .. i)) + dt.print(string.format(_("set zoom level to %d"), i)) sleep(1500) end end @@ -174,12 +175,12 @@ dt.print_log("starting sorts") for n, sort in ipairs(sorts) do dt.gui.libs.filter.sort(sort) - dt.print(_("set lighttable sort to " .. sort)) + dt.print(string.format(_("set lighttable sort to %s"), sort)) sleep(1500) for m, sort_order in ipairs(sort_orders) do dt.gui.libs.filter.sort_order(sort_order) - dt.print(_("sort order set to " .. sort_order)) + dt.print(string.format(_("sort order set to %s"), sort_order)) sleep(1500) end end @@ -190,12 +191,12 @@ dt.print(_("lighttable filtering demonstration")) for n, rating in ipairs(ratings) do dt.gui.libs.filter.rating(rating) - dt.print(_("set filter to " .. rating)) + dt.print(string.format(_("set filter to %s"), rating)) sleep(1500) for m, rating_comparator in ipairs(rating_comparators) do dt.gui.libs.filter.rating_comparator(rating_comparator) - dt.print(_("set rating comparator to " .. rating_comparator)) + dt.print(string.format(_("set rating comparator to %s"), rating_comparator)) sleep(1500) end end @@ -215,6 +216,14 @@ current_sort_order = dt.gui.libs.filter.sort_order(current_sort_order) -- it's time to destroy the script and then return the data to -- script_manager local script_data = {} + +script_data.metadata = { + 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" +} + script_data.destroy = destroy return script_data diff --git a/examples/moduleExample.lua b/examples/moduleExample.lua index b68161e8..89c56bc2 100644 --- a/examples/moduleExample.lua +++ b/examples/moduleExample.lua @@ -35,25 +35,31 @@ local du = require "lib/dtutils" du.check_min_api_version("7.0.0", "moduleExample") +-- https://www.darktable.org/lua-api/index.html#darktable_gettext +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + script_data.destroy = nil -- function to destory the script script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them -- translation --- https://www.darktable.org/lua-api/index.html#darktable_gettext -local gettext = dt.gettext - -gettext.bindtextdomain("moduleExample", dt.configuration.config_dir .. "/lua/locale/") - -local function _(msgid) - return gettext.dgettext("moduleExample", msgid) -end - -- declare a local namespace and a couple of variables we'll need to install the module local mE = {} mE.widgets = {} @@ -69,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 @@ -79,9 +85,9 @@ local function install_module() orientation = "vertical", dt.new_widget("button") { - label = _("MyButton"), + label = _("my ") .. "button", clicked_callback = function (_) - dt.print(_("Button clicked")) + dt.print(_("button clicked")) end }, table.unpack(mE.widgets), @@ -104,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 = _("MyCheck_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 = _("MyCombobox"), 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") @@ -116,21 +122,21 @@ local entry = dt.new_widget("entry") placeholder = _("placeholder"), is_password = false, editable = true, - tooltip = _("Tooltip Text"), + tooltip = _("tooltip text"), reset_callback = function(self) self.text = "text" end } -- https://www.darktable.org/lua-api/types_lua_file_chooser_button.html local file_chooser_button = dt.new_widget("file_chooser_button") { - title = _("MyFile_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 = _("MyLabel") -- 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"){} @@ -138,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 = _("MySlider"), + 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 4ab650ff..21dc0cb1 100644 --- a/examples/multi_os.lua +++ b/examples/multi_os.lua @@ -70,13 +70,10 @@ local dtsys = require "lib/dtutils.system" -- system utilities translations, inserting this lays the groundwork for anyone who wants to translate the strings. ]] -local gettext = dt.gettext - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("multi_os",dt.configuration.config_dir.."/lua/locale/") +local gettext = dt.gettext.gettext local function _(msgid) - return gettext.dgettext("multi_os", msgid) + return gettext(msgid) end --[[ @@ -187,12 +184,12 @@ local function extract_embedded_jpeg(images) end else dt.print_error(image.filename .. " is not a raw file. No image can be extracted") -- print debugging error message - dt.print(image.filename .. " is not a raw file. No image can be extracted") -- print the error to the screen + dt.print(string.format(_("%s is not a raw file, no image can be extracted"), image.filename)) -- print the error to the screen end end else dt.print_error("ufraw-batch not found. Exiting...") -- print debugging error message - dt.print("ufraw-batch not found. Exiting...") -- print the error to the screen + dt.print("ufraw-batch not found, exiting...") -- print the error to the screen end end @@ -216,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) @@ -227,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 ) @@ -241,7 +238,7 @@ end dt.gui.libs.image.register_action( "multi_os", _("extract embedded jpeg"), function(event, images) extract_embedded_jpeg(images) end, - "extract embedded jpeg" + _("extract embedded jpeg") ) --[[ @@ -251,7 +248,7 @@ dt.gui.libs.image.register_action( dt.register_event( "multi_os", "shortcut", function(event, shortcut) extract_embedded_jpeg(dt.gui.action_images) end, - "extract embedded jpeg" + _("extract embedded jpeg") ) --[[ @@ -261,6 +258,14 @@ dt.register_event( ]] local script_data = {} + +script_data.metadata = { + 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" +} + script_data.destroy = destroy return script_data diff --git a/examples/panels_demo.lua b/examples/panels_demo.lua index 1f74d718..ac9e3de4 100644 --- a/examples/panels_demo.lua +++ b/examples/panels_demo.lua @@ -53,17 +53,16 @@ end -- C O N S T A N T S -- - - - - - - - - - - - - - - - - - - - - - - - -local MODULE_NAME = "panels" +local MODULE_NAME = "panels_demo" local PS = dt.configuration.running_os == "windows" and "\\" or "/" -- - - - - - - - - - - - - - - - - - - - - - - - -- T R A N S L A T I O N S -- - - - - - - - - - - - - - - - - - - - - - - - -local gettext = dt.gettext -gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS) +local gettext = dt.gettext.gettext local function _(msgid) - return gettext.dgettext(MODULE_NAME, msgid) + return gettext(msgid) end -- - - - - - - - - - - - - - - - - - - - - - - - @@ -94,29 +93,29 @@ 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 -- display left, then top, then right, then bottom - dt.print(_("Make panels visible, one at a time")) + dt.print(_("make panels visible, one at a time")) 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 -- hide all -dt.print(_("Hiding all panels")) +dt.print(_("hiding all panels")) sleep(1500) dt.gui.panel_hide_all() @@ -124,7 +123,7 @@ sleep(1500) -- show all -dt.print(_("Showing all panels")) +dt.print(_("showing all panels")) sleep(1500) dt.gui.panel_show_all() @@ -132,7 +131,7 @@ sleep(1500) -- restore -dt.print(_("Restoring panels to starting configuration")) +dt.print(_("restoring panels to starting configuration")) for i = 1, #panels do if panel_status[i] then dt.gui.panel_show(panels[i]) @@ -145,6 +144,14 @@ end -- it's time to destroy the script and then return the data to -- script_manager local script_data = {} + +script_data.metadata = { + 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" +} + script_data.destroy = destroy return script_data diff --git a/examples/preferenceExamples.lua b/examples/preferenceExamples.lua index 99f85188..84425744 100644 --- a/examples/preferenceExamples.lua +++ b/examples/preferenceExamples.lua @@ -24,28 +24,45 @@ USAGE local dt = require "darktable" local du = require "lib/dtutils" +-- translation facilities + +local gettext = dt.gettext.gettext + +local function _(msg) + return gettext(msg) +end + +local script_data = {} + +script_data.metadata = { + 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" +} + 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 @@ -53,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 @@ -63,22 +80,30 @@ 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 + +local function destroy() + -- nothing to destroy +end + +script_data.destroy = destroy + +return script_data diff --git a/examples/printExamples.lua b/examples/printExamples.lua index b220b7a0..721138cd 100644 --- a/examples/printExamples.lua +++ b/examples/printExamples.lua @@ -24,6 +24,14 @@ local du = require "lib/dtutils" du.check_min_api_version("5.0.0", "printExamples") +-- translation facilities + +local gettext = dt.gettext.gettext + +local function _(msg) + return gettext(msg) +end + -- script_manager integration to allow a script to be removed -- without restarting darktable local function destroy() @@ -32,7 +40,7 @@ end -- Will print a string to the darktable control log (the long -- overlayed window that appears over the main panel). -dt.print("print") +dt.print(_("print")) -- This function will print its parameter if the Lua logdomain is -- activated. Start darktable with the "-d lua" command line option @@ -48,6 +56,14 @@ dt.print_log("print log") -- it's time to destroy the script and then return the data to -- script_manager local script_data = {} + +script_data.metadata = { + 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" +} + script_data.destroy = destroy return script_data diff --git a/examples/running_os.lua b/examples/running_os.lua index d214b2a9..69741288 100644 --- a/examples/running_os.lua +++ b/examples/running_os.lua @@ -30,18 +30,34 @@ local du = require "lib/dtutils" du.check_min_api_version("5.0.0", "running_os") +-- translation facilities + +local gettext = dt.gettext.gettext + +local function _(msg) + return gettext(msg) +end + -- script_manager integration to allow a script to be removed -- without restarting darktable local function destroy() -- nothing to destroy end -dt.print("You are running: "..dt.configuration.running_os) +dt.print(string.format(_("you are running: %s"), dt.configuration.running_os)) -- 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 -- script_manager local script_data = {} + +script_data.metadata = { + 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" +} + script_data.destroy = destroy return script_data diff --git a/examples/x-touch.lua b/examples/x-touch.lua new file mode 100644 index 00000000..7da3d54c --- /dev/null +++ b/examples/x-touch.lua @@ -0,0 +1,208 @@ +--[[ + This file is part of darktable, + copyright (c) 2023 Diederik ter Rahe + + 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 . +]] +--[[ +X-Touch Mini flexible encoder shortcuts + +This script will create virtual sliders that are mapped dynamically to +the most relevant sliders for the currently focused processing module. +Tailored modules are color zones, tone equalizer, color calibration and +mask manager properties. The script can easily be amended for other +devices or personal preferences. Virtual "toggle" buttons can be created +as well, that dynamically change meaning depending on current status. + +USAGE +* require this script from your main lua file +* restart darktable +* create shortcuts for each of the encoders on the x-touch mini + to a virtual slider under lua/x-touch + or import the following shortcutsrc file in the shortcuts dialog/preferences tab: + +None;midi:CC1=lua/x-touch/knob 1 +None;midi:CC2=lua/x-touch/knob 2 +None;midi:CC3=lua/x-touch/knob 3 +None;midi:CC4=lua/x-touch/knob 4 +None;midi:CC5=lua/x-touch/knob 5 +None;midi:CC6=lua/x-touch/knob 6 +None;midi:CC7=lua/x-touch/knob 7 +None;midi:CC8=lua/x-touch/knob 8 +midi:E0=global/modifiers +midi:F0=global/modifiers;ctrl +midi:F#0=global/modifiers;alt +midi:G#-1=iop/blend/tools/show and edit mask elements +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" +local du = require "lib/dtutils" + +du.check_min_api_version("9.2.0", "x-touch") + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + 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" +} + +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 + +-- set up 8 mimic sliders with the same function +for k = 1,8 do + dt.gui.mimic("slider", "knob ".. k, + function(action, element, effect, size) + -- take the number from the mimic name + local k = tonumber(action:sub(-1)) + + -- only operate in darkroom; return NAN otherwise + if dt.gui.current_view() ~= dt.gui.views.darkroom then + return 0/0 + end + + local maskval = 0/0 + if k < 8 then + -- first try if the mask slider at that position is active + local s = { "opacity", + "size", + "feather", + "hardness", + "rotation", + "curvature", + "compression" } + maskval = dt.gui.action("lib/masks/properties/" .. s[k], + element, effect, size) + end + -- if a value different from NAN is returned, the slider was active + if maskval == maskval then + return maskval + + -- try if colorzones module is focused; if so select element of graph + elseif dt.gui.action("iop/colorzones", "focus") ~= 0 then + which = "iop/colorzones/graph" + local e = { "red", + "orange", + "yellow", + "green", + "aqua", + "blue", + "purple", + "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 + elseif dt.gui.action("iop/sigmoid", "focus") ~= 0 and k <8 then + local e = { "red attenuation", "red rotation", "green attenuation", "green rotation", "blue attenuation", "blue rotation", "recover purity" } + which = "iop/sigmoid/primaries/"..e[k] + + -- if the rgb primaries is focused, + -- check sliders + elseif dt.gui.action("iop/primaries", "focus") ~= 0 and k >=1 then + local e = { "red hue", "red purity", "green hue", "green purity", "blue hue", "blue purity", "tint hue", "tint purity" } + which = "iop/primaries/" ..e[k] + + -- if the tone equalizer is focused, + -- select one of the sliders in the "simple" tab + elseif dt.gui.action("iop/toneequal", "focus") ~= 0 then + which ="iop/toneequal/simple/"..(k-9).." EV" + + -- if color calibration is focused, + -- the last 4 knobs are sent there + elseif dt.gui.action("iop/channelmixerrgb", "focus") ~= 0 + and k >= 5 then + -- knob 5 selects the active tab; pressing it resets to CAT + if k == 5 then + which = "iop/channelmixerrgb/page" + element = "CAT" + -- since the tab action is not a slider, + -- the effects need to be translated + if effect == "up" then effect = "next" + elseif effect == "down" then effect = "previous" + else effect = "activate" + end + else + -- knobs 6, 7 and 8 are for the three color sliders on each tab + which = "iop/focus/sliders" + local e = { "1st", + "2nd", + "3rd" } + element = e[k - 5] + end + + -- the 4th knob is contrast; + -- either colorbalance if it is focused, or filmic + elseif dt.gui.action("iop/colorbalancergb", "focus") ~= 0 + and k == 4 then + which = "iop/colorbalancergb/contrast" + + -- in all other cases use a default selection + else + local s = { "iop/exposure/exposure", + "iop/filmicrgb/white relative exposure", + "iop/filmicrgb/black relative exposure", + "iop/filmicrgb/contrast", + "iop/crop/left", + "iop/crop/right", + "iop/crop/top", + "iop/crop/bottom" } + which = s[k] + end + + -- now pass the element/effect/size to the selected slider + return dt.gui.action(which, element, effect, size) + end) +end + +local function destroy() + -- nothing to destroy +end + +script_data.destroy = destroy + +return script_data diff --git a/lib/dtutils.lua b/lib/dtutils.lua index 0a6d4416..e6595865 100644 --- a/lib/dtutils.lua +++ b/lib/dtutils.lua @@ -61,14 +61,50 @@ 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.") + dt.control.sleep(2000) -- allow time for the error to display before script_manager writes it's error message error("Minimum API " .. min_api .. " not met for " .. script_name .. ".", 0) end end +dtutils.libdoc.functions["check_max_api_version"] = { + Name = [[check_max_api_version]], + Synopsis = [[check the maximum required api version against the current api version]], + Usage = [[local du = require "lib/dtutils" + + local result = du.check_max_api_version(max_api, script_name) + max_api - string - the api version that the application was written for (example: "5.0.0") + script_name - string - the name of the script]], + Description = [[check_max_api_version compares the maximum api required for the appllication to + 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.]], + 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.]], + Example = [[check_max_api_version("9.0.0") does nothing if the api is less than or equal to 9.0.0 otherwise an + error message is printed and an error is thrown stopping execution of the script.]], + See_Also = [[]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +function dtutils.check_max_api_version(max_api, script_name) + local current_api = dt.configuration.api_version_string + 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.") + dt.control.sleep(2000) -- allow time for the error to display before script_manager writes it's error message + error("Maximum API " .. max_api .. " not met for " .. script_name .. ".", 0) + end +end + dtutils.libdoc.functions["split"] = { Name = [[split]], Synopsis = [[split a string on a specified separator]], @@ -184,7 +220,8 @@ function dtutils.prequire(req_name) if status then log.msg(log.info, "Loaded " .. req_name) else - log.msg(log.info, "Error loading " .. req_name) + log.msg(log.error, "Error loading " .. req_name) + log.msg(log.error, "Error returned is " .. lib) end return status, lib end @@ -337,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 = [[]], @@ -354,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/locale/de_DE/LC_MESSAGES/select_non_existing.po b/locale/de_DE/LC_MESSAGES/select_non_existing.po new file mode 100644 index 00000000..c36ff806 --- /dev/null +++ b/locale/de_DE/LC_MESSAGES/select_non_existing.po @@ -0,0 +1,12 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" + +msgid "select non existing images" +msgstr "nicht vorhandene Bilder auswählen" + +msgid "select non existing" +msgstr "nicht vorhandene auswählen" + +msgid "select all non-existing images in the current images" +msgstr "alle nicht vorhandenen Bilder in den aktuellen Bildern auswählen" diff --git a/official/apply_camera_style.lua b/official/apply_camera_style.lua new file mode 100644 index 00000000..b748ef95 --- /dev/null +++ b/official/apply_camera_style.lua @@ -0,0 +1,494 @@ +--[[ + + 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 camera styles|" + +-- 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, "%-", "%%-") + -- 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[2] = string.lower(parts[2]) + log.msg(log.debug, "maker is " .. parts[2]) + + if not acs.styles[parts[2]] then + acs.styles[parts[2]] = {} + acs.styles[parts[2]]["styles"] = {} + acs.styles[parts[2]]["patterns"] = {} + end + if parts[3] then + if not string.match(parts[3], "]") then + table.insert(acs.styles[parts[2]].styles, style) + local processed_pattern = process_pattern(parts[#parts]) + table.insert(acs.styles[parts[2]].patterns, processed_pattern) + log.msg(log.debug, "pattern for " .. style.name .. " is " .. processed_pattern) + else + local processed_patterns = process_set(parts[3]) + for _, pat in ipairs(processed_patterns) do + table.insert(acs.styles[parts[2]].styles, style) + table.insert(acs.styles[parts[2]].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 5f1b4848..aeb45edb 100644 --- a/official/check_for_updates.lua +++ b/official/check_for_updates.lua @@ -21,7 +21,7 @@ a simple script that will automatically look for newer releases on github and in when there is something. it will only check on startup and only once a week. USAGE -* install luasec and cjson for Lua 5.2 on your system +* install luasec and cjson for Lua 5.4 on your system * require this script from your main lua file * restart darktable @@ -34,13 +34,27 @@ local cjson = require "cjson" du.check_min_api_version("2.0.0", "check_for_updates") +local gettext = dt.gettext.gettext + +local function _(msg) + return gettext(msg) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + 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 -- compare two version strings of the form "major.minor.patch" -- returns -1, 0, 1 if the first version is smaller, equal, greater than the second version, diff --git a/official/copy_paste_metadata.lua b/official/copy_paste_metadata.lua index ec4e7c85..00040c9b 100644 --- a/official/copy_paste_metadata.lua +++ b/official/copy_paste_metadata.lua @@ -27,17 +27,29 @@ USAGE local dt = require "darktable" local du = require "lib/dtutils" -local gettext = dt.gettext +local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "copy_paste_metadata") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.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" +} + 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 -- set this to "false" if you don't want to overwrite metadata fields -- (title, description, creator, publisher and rights) that are already set @@ -57,13 +69,6 @@ local publisher = "" local rights = "" local tags = {} --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("copy_paste_metadata",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("copy_paste_metadata", msgid) -end - local function copy(image) if not image then have_data = false @@ -146,13 +151,13 @@ dt.gui.libs.image.register_action( dt.register_event( "capmd1", "shortcut", function(event, shortcut) copy(dt.gui.action_images[1]) end, - "copy metadata" + _("copy metadata") ) dt.register_event( "capmd2", "shortcut", function(event, shortcut) paste(dt.gui.action_images) end, - "paste metadata" + _("paste metadata") ) script_data.destroy = destroy diff --git a/official/delete_long_tags.lua b/official/delete_long_tags.lua index a5f54553..c770402b 100644 --- a/official/delete_long_tags.lua +++ b/official/delete_long_tags.lua @@ -33,13 +33,27 @@ local du = require "lib/dtutils" du.check_min_api_version("2.0.0", "delete_long_tags") +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + 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 dt.preferences.register("delete_long_tags", "length", "integer", "maximum length of tags to keep", @@ -58,7 +72,7 @@ local long_tags = {} for _,t in ipairs(dt.tags) do local len = #t.name if len > max_length then - print("deleting tag `"..t.name.."' (length: "..len..")") + dt.print_log("deleting tag `"..t.name.."' (length: "..len..")") table.insert(long_tags, t.name) end end diff --git a/official/delete_unused_tags.lua b/official/delete_unused_tags.lua index fcaf03d6..3815aea0 100644 --- a/official/delete_unused_tags.lua +++ b/official/delete_unused_tags.lua @@ -37,13 +37,27 @@ local function destroy() -- noting to destroy end +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + 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 -- deleting while iterating the tags list seems to break the iterator! local unused_tags = {} @@ -55,7 +69,7 @@ for _, t in ipairs(dt.tags) do end for _,name in pairs(unused_tags) do - print("deleting tag `" .. name .. "'") + dt.print_log("deleting tag `" .. name .. "'") tag = dt.tags.find(name) tag:delete() end diff --git a/official/enfuse.lua b/official/enfuse.lua index 212f0e3f..7bda2cf4 100644 --- a/official/enfuse.lua +++ b/official/enfuse.lua @@ -39,35 +39,40 @@ local dtsys = require "lib/dtutils.system" local PS = dt.configuration.running_os == "windows" and "\\" or "/" -local gettext = dt.gettext +local gettext = dt.gettext.gettext du.check_min_api_version("7.0.0", "enfuse") +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + name = _("enfuse"), + purpose = _("exposure blend images"), + author = "Tobias Ellinghaus", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/enfuse" +} + 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 - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("enfuse",dt.configuration.config_dir..PS .. "lua" .. PS .. "locale" .. PS) +script_data.show = nil -- only required for libs since the destroy_method only hides them local enf = {} enf.event_registered = false enf.module_installed = false enf.lib_widgets = {} -local function _(msgid) - return gettext.dgettext("enfuse", msgid) -end - 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 @@ -130,8 +135,8 @@ if enfuse_installed then if version < "4.2" then exposure_mu = dt.new_widget("slider") { - label = "exposure mu", - tooltip = "center also known as MEAN of Gaussian weighting function (0 <= MEAN <= 1); default: 0.5", + 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, value = dt.preferences.read("enfuse", "exposure_mu", "float") @@ -139,8 +144,8 @@ if enfuse_installed then else exposure_mu = dt.new_widget("slider") { - label = "exposure optimum", - tooltip = "optimum exposure value, usually the maximum of the weighting function (0 <= OPTIMUM <=1); default 0.5", + 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, value = dt.preferences.read("enfuse", "exposure_optimum", "float") @@ -149,8 +154,8 @@ if enfuse_installed then local depth = dt.new_widget("combobox") { - label = "depth", - tooltip = "the number of bits per channel of the output image", + 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, "8", "16", "32" @@ -158,15 +163,15 @@ if enfuse_installed then local blend_colorspace = dt.new_widget("combobox") { - label = "blend colorspace", - tooltip = "Force blending in selected 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" } local enfuse_button = dt.new_widget("button") { - label = enfuse_installed and "run enfuse" or "enfuse not installed", + label = enfuse_installed and _("run enfuse") or _("enfuse not installed"), clicked_callback = function () -- remember exposure_mu -- TODO: find a way to save it whenever the value changes @@ -184,7 +189,7 @@ if enfuse_installed then end local f = io.open(response_file, "w") if not f then - dt.print(string.format(_("Error writing to `%s`"), response_file)) + dt.print(string.format(_("error writing to '%s'"), response_file)) os.remove(response_file) return end @@ -207,9 +212,9 @@ if enfuse_installed then if dt.configuration.running_os == "windows" then tmp_exported = dt.configuration.tmp_dir .. tmp_exported -- windows os.tmpname() defaults to root directory end - dt.print(string.format(_("Converting raw file '%s' to tiff..."), i.filename)) + dt.print(string.format(_("converting raw file '%s' to tiff..."), i.filename)) tiff_exporter:write_image(i, tmp_exported, false) - dt.print_log(string.format("Raw file '%s' converted to '%s'", i.filename, tmp_exported)) + dt.print_log(string.format("raw file '%s' converted to '%s'", i.filename, tmp_exported)) cnt = cnt + 1 f:write(tmp_exported.."\n") @@ -217,14 +222,14 @@ if enfuse_installed then -- other images will be skipped else - dt.print(string.format(_("Skipping %s..."), i.filename)) + dt.print(string.format(_("skipping %s..."), i.filename)) n_skipped = n_skipped + 1 end end f:close() -- bail out if there is nothing to do if cnt == 0 then - dt.print(_("No suitable images selected, nothing to do for enfuse")) + dt.print(_("no suitable images selected, nothing to do for enfuse")) os.remove(response_file) return end @@ -250,7 +255,7 @@ if enfuse_installed then ..blend_colorspace_option .." -o \""..output_image.."\" \"@"..response_file.."\"" if dtsys.external_command( command) > 0 then - dt.print(_("Enfuse failed, see terminal output for details")) + dt.print(_("enfuse failed, see terminal output for details")) os.remove(response_file) return end @@ -298,7 +303,7 @@ if enfuse_installed then else dt.print_error("enfuse executable not found") error("enfuse executable not found") - dt.print(_("Could not find enfuse executable. Not loading enfuse exporter...")) + dt.print(_("could not find enfuse executable, not loading enfuse exporter...")) end script_data.destroy = destroy diff --git a/official/generate_image_txt.lua b/official/generate_image_txt.lua index 11eceb0b..c786aabf 100644 --- a/official/generate_image_txt.lua +++ b/official/generate_image_txt.lua @@ -38,34 +38,48 @@ local dt = require "darktable" local du = require "lib/dtutils" require "darktable.debug" +local gettext = dt.gettext.gettext + +local function _(msg) + return gettext(msg) +end + du.check_min_api_version("7.0.0", "generate_image_txt") -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + 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 dt.preferences.register("generate_image_txt", "enabled", "bool", - "create txt sidecars to display with images", - "the txt files created get shown when the lighttable is zoomed in to one image. also enable the txt overlay setting in the gui tab", + _("create txt sidecars to display with images"), + _("the txt files created get shown when the lighttable is zoomed in to one image. also enable the txt overlay setting in the gui tab"), false) dt.preferences.register("generate_image_txt", "command", "string", - "command to generate the txt sidecar", - "the output of this command gets written to the txt file. use $(FILE_NAME) for the image file", + _("command to generate the txt sidecar"), + _("the output of this command gets written to the txt file. use $(FILE_NAME) for the image file"), "exiv2 $(FILE_NAME)") local check_command = function(command) if not command:find("$(FILE_NAME)", 1, true) then - dt.print("the command for txt sidecars looks bad. better check the preferences") + dt.print(_("the command for txt sidecars looks bad. better check the preferences")) end end diff --git a/official/image_path_in_ui.lua b/official/image_path_in_ui.lua index e256cf41..2f23184a 100644 --- a/official/image_path_in_ui.lua +++ b/official/image_path_in_ui.lua @@ -33,13 +33,27 @@ local du = require "lib/dtutils" du.check_min_api_version("7.0.0", "image_path_in_ui") +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + 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 local ipiu = {} ipiu.module_installed = false @@ -49,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 e27a1f7f..adf83620 100644 --- a/official/import_filter_manager.lua +++ b/official/import_filter_manager.lua @@ -32,19 +32,40 @@ USAGE local dt = require "darktable" +local gettext = dt.gettext.gettext + +local function _(msg) + return gettext(msg) +end + +local script_data = {} + +script_data.metadata = { + 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" +} + +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 + local import_filter_list = {} local n_import_filters = 1 -- allow changing the filter from the preferences dt.preferences.register("import_filter_manager", "active_filter", "string", - "import filter", "the name of the filter used for importing images", "") + _("import filter"), _("the name of the filter used for importing images"), "") -- the dropdown to select the active filter from the import dialog local filter_dropdown = dt.new_widget("combobox") { - label = "import filter", + label = _("import filter"), editable = false, + tooltip = _("import filters are applied after completion of the import dialog"), changed_callback = function(widget) dt.preferences.write("import_filter_manager", "active_filter", "string", widget.value) @@ -74,6 +95,12 @@ dt.register_import_filter = function(name, callback) if name == active_filter then filter_dropdown.value = n_import_filters end end +local function destroy() + --noting to destroy +end + +script_data.destroy = destroy +return script_data -- vim: shiftwidth=2 expandtab tabstop=2 cindent -- kate: tab-indents: off; indent-width 2; replace-tabs on; remove-trailing-space on; diff --git a/official/import_filters.lua b/official/import_filters.lua index 584a2718..87d8b0ef 100644 --- a/official/import_filters.lua +++ b/official/import_filters.lua @@ -30,11 +30,31 @@ USAGE local dt = require "darktable" +local gettext = dt.gettext.gettext + +local function _(msg) + return gettext(msg) +end + +local script_data = {} + +script_data.metadata = { + 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" +} + +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 + -- we get fed a sorted list of filenames. just setting images to ignore to nil is enough -- ignore jpeg dt.register_import_filter("ignore jpegs", function(event, images) - dt.print_error("ignoring all jpegs") + dt.print_log("ignoring all jpegs") for i, img in ipairs(images) do local extension = img:match("[^.]*$"):upper() if (extension == "JPG") or (extension == "JPEG") then @@ -88,5 +108,13 @@ dt.register_import_filter("prefer raw over jpeg", function(event, images) end) +local function destroy() + -- nothing to destroy +end + +script_data.destroy = destroy + +return script_data + -- vim: shiftwidth=2 expandtab tabstop=2 cindent -- kate: tab-indents: off; indent-width 2; replace-tabs on; remove-trailing-space on; diff --git a/official/save_selection.lua b/official/save_selection.lua index d05a760a..64d33b4e 100644 --- a/official/save_selection.lua +++ b/official/save_selection.lua @@ -38,13 +38,27 @@ local du = require "lib/dtutils" du.check_min_api_version("7.0.0", "save_selection") +local gettext = dt.gettext.gettext + +local function _(msg) + return gettext(msg) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + 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 local buffer_count = 5 @@ -60,16 +74,16 @@ for i = 1, buffer_count do local saved_selection dt.register_event("save_selection save " .. i, "shortcut", function() saved_selection = dt.gui.selection() - end, "save to buffer " .. i) + end, string.format(_("save to buffer %d"), i)) dt.register_event("save_selection restore " .. i, "shortcut", function() dt.gui.selection(saved_selection) - end, "restore from buffer " .. i) + end, string.format(_("restore from buffer %d"), i)) end local bounce_buffer = {} dt.register_event("save_selection switch", "shortcut", function() bounce_buffer = dt.gui.selection(bounce_buffer) -end, "switch selection with temporary buffer") +end, _("switch selection with temporary buffer")) script_data.destroy = destroy diff --git a/official/selection_to_pdf.lua b/official/selection_to_pdf.lua index b2b688e5..64b9c8be 100644 --- a/official/selection_to_pdf.lua +++ b/official/selection_to_pdf.lua @@ -38,26 +38,40 @@ local du = require "lib/dtutils" du.check_min_api_version("7.0.0", "selection_to_pdf") +local gettext = dt.gettext.gettext + +local function _(msg) + return gettext(msg) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + 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 dt.preferences.register ("selection_to_pdf","Open with","string", - "a pdf viewer", - "Can be an absolute pathname or the tool may be in the PATH", + _("a pdf viewer"), + _("can be an absolute pathname or the tool may be in the PATH"), "xdg-open") local title_widget = dt.new_widget("entry") { - placeholder="Title" + placeholder = _("title") } local no_of_thumbs_widget = dt.new_widget("slider") { - label = "Thumbs per Line", + label = _("thumbs per line"), soft_min = 1, -- The soft minimum value for the slider, the slider can't go beyond this point soft_max = 10, -- The soft maximum value for the slider, the slider can't go beyond this point hard_min = 1, -- The hard minimum value for the slider, the user can't manually enter a value beyond this point @@ -65,10 +79,10 @@ local no_of_thumbs_widget = dt.new_widget("slider") value = 4 -- The current value of the slider } local widget = dt.new_widget("box") { - orientation=horizontal, - dt.new_widget("label"){label = "Title:"}, + orientation = horizontal, + dt.new_widget("label"){label = _("title:")}, title_widget, - dt.new_widget("label"){label = "Thumbnails per row:"}, + dt.new_widget("label"){label = _("thumbnails per row:")}, no_of_thumbs_widget } @@ -115,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 @@ -162,7 +176,7 @@ dt.register_storage("export_pdf","Export thumbnails to pdf", local command = "pdflatex -halt-on-error -output-directory "..dir.." "..locfile local result = dt.control.execute(command) if result ~= 0 then - dt.print("Problem running pdflatex") -- this one is probably usefull to the user + dt.print(_("problem running pdflatex")) -- this one is probably usefull to the user error("Problem running "..command) end @@ -172,7 +186,7 @@ dt.register_storage("export_pdf","Export thumbnails to pdf", command = command.." "..pdffile local result = dt.control.execute(command) if result ~= 0 then - dt.print("Problem running pdf viewer") -- this one is probably usefull to the user + dt.print(_("problem running pdf viewer")) -- this one is probably usefull to the user error("Problem running "..command) end diff --git a/tools/executable_manager.lua b/tools/executable_manager.lua index 68752880..4d03be28 100644 --- a/tools/executable_manager.lua +++ b/tools/executable_manager.lua @@ -31,23 +31,34 @@ 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 + +local function _(msg) + return gettext(msg) +end + -- return data structure for script_manager local script_data = {} +script_data.metadata = { + 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" +} + 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 local PS = dt.configuration.running_os == "windows" and "\\" or "/" -local gettext = dt.gettext - -gettext.bindtextdomain("executable_manager",dt.configuration.config_dir.."/lua/locale/") - local exec_man = {} -- our own namespace exec_man.module_installed = false exec_man.event_registered = false @@ -56,10 +67,6 @@ exec_man.event_registered = false -- F U N C T I O N S -- - - - - - - - - - - - - - - - - - - - - - - - - - - - -local function _(msg) - return gettext.dgettext("executable_manager", msg) -end - local function grep(file, pattern) local result = {} @@ -99,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", 100}}, -- containers + {[dt.gui.views.lighttable] = {panel, panel_pos}}, -- containers dt.new_widget("box") -- widget { orientation = "vertical", @@ -141,7 +154,7 @@ local matches = grep(DARKTABLERC, "executable_paths") -- check if we have something to manage and exit if not if #matches == 0 then - dt.print(_("No executable paths found, exiting...")) + dt.print(_("no executable paths found, exiting...")) return end @@ -166,7 +179,7 @@ for i,exec in ipairs(exec_table) do editable = false } executable_path_widgets[exec] = dt.new_widget("file_chooser_button"){ - title = _("select ") .. exec .. _(" executable"), + title = _(string.format("select %s executable", exec)), value = df.get_executable_path_preference(exec), is_directory = false, changed_callback = function(self) @@ -186,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) @@ -209,7 +222,7 @@ for i,exec in ipairs(exec_table) do dt.new_widget("section_label"){label = _("reset")}, dt.new_widget("button"){ label = _("clear"), - tooltip = _("Clear path for ") .. exec, + tooltip = string.format(_("clear path for %s"), exec), clicked_callback = function() df.set_executable_path_preference(exec, "") executable_path_widgets[exec].value = "" diff --git a/tools/gen_i18n_mo.lua b/tools/gen_i18n_mo.lua deleted file mode 100644 index 6aa453c2..00000000 --- a/tools/gen_i18n_mo.lua +++ /dev/null @@ -1,108 +0,0 @@ ---[[ - gen_18n_mo.lua - generate .mo files from .po files and put them in the correct place - - Copyright (C) 2016,2018 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 . -]] ---[[ - gen_i18n_mo - generate translation files from the source and place them in the appropriate locale directory - - gen_i18n_mo finds all the .po files scattered throughout the script tree, compiles them into - .mo files and places them in the correct locale directory for use by the gettext tools. - -]] - -local dt = require "darktable" -local du = require "lib/dtutils" -local df = require "lib/dtutils.file" -local log = require "lib/dtutils.log" -local dtsys = require "lib/dtutils.system" - -du.check_min_api_version("5.0.0", "gen_I18n_mo") - -local function destroy() - -- nothing to destroy -end - --- figure out the path separator - -local PS = dt.configuration.running_os == "windows" and "\\" or "/" - -local LUA_DIR = dt.configuration.config_dir .. PS .. "lua" .. PS -local LOCALE_DIR = dt.configuration.config_dir .. PS .. "lua" .. PS .. "locale" .. PS - --- check if we have msgfmt - -local msgfmt_executable = df.check_if_bin_exists("msgfmt") - -if msgfmt_executable then - - -- find the .po files - - local find_cmd = "find -L " .. LUA_DIR .. " -name \\*.po -print" - if dt.configuration.running_os == "windows" then - find_cmd = "dir /b/s " .. LUA_DIR .. "\\*.po" - end - - local output = io.popen(find_cmd) - - -- for each .po file.... - - for line in output:lines() do - local fname = df.get_filename(line) - - -- get the language used... this depends on the file being named using - -- the convention /..../lang/LC_MESSAGES/file.po where lang is de_DE, fr_FR, etc. - - local path_parts = du.split(line, PS) - local lang = path_parts[#path_parts - 2] - - -- ensure there is a destination directory for them - - local mkdir_cmd = "mkdir -p " - if dt.configuration.running_os == "windows" then - mkdir_cmd = "mkdir " - end - - if not df.check_if_file_exists(LOCALE_DIR .. lang .. PS .. "LC_MESSAGES") then - log.msg(log.info, "Creating locale", lang) - os.execute(mkdir_cmd .. LOCALE_DIR .. lang .. PS .. "LC_MESSAGES") - end - - -- generate the mo file - - fname = string.gsub(fname, ".po$", ".mo") - log.msg(log.info, "Compiling translation to", fname) - local result = os.execute(msgfmt_executable .. " -o " .. LOCALE_DIR .. lang .. PS .. "LC_MESSAGES" .. PS .. fname .. " " .. line) - end -else - log.msg(log.screen, "ERROR: msgfmt executable not found. Please install or specifiy location in preferences.") -end -dt.preferences.register("executable_paths", "msgfmt", -- name - "file", -- type - 'gen_i18n_mo: msgfmt location', -- label - 'Install location of msgfmt. Requires restart to take effect.', -- tooltip - "msgfmt", -- default - dt.new_widget("file_chooser_button"){ - title = "Select msgfmt[.exe] file", - value = "", - is_directory = false, - } -) - -local script_data = {} -script_data.destroy = destroy - -return script_data diff --git a/tools/get_lib_manpages.lua b/tools/get_lib_manpages.lua index 2e599181..0e641104 100644 --- a/tools/get_lib_manpages.lua +++ b/tools/get_lib_manpages.lua @@ -7,11 +7,18 @@ 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 du.check_min_api_version("3.0.0", "get_lib_manpages") +local gettext = dt.gettext.gettext + +local function _(msg) + return gettext(msg) +end + local function destroy() -- nothing to destroy end @@ -79,6 +86,14 @@ for line in output:lines() do end local script_data = {} + +script_data.metadata = { + 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" +} + script_data.destroy = destroy return script_data diff --git a/tools/get_libdoc.lua b/tools/get_libdoc.lua index d22516fc..2c4458d0 100644 --- a/tools/get_libdoc.lua +++ b/tools/get_libdoc.lua @@ -6,9 +6,16 @@ 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 + +local function _(msg) + return gettext(msg) +end + local function destroy() -- nothing to destroy end @@ -53,6 +60,14 @@ for line in output:lines() do end local script_data = {} + +script_data.metadata = { + 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" +} + script_data.destroy = destroy return script_data diff --git a/tools/script_manager.lua b/tools/script_manager.lua index 7befa037..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 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,61 +56,53 @@ local dtsys = require "lib/dtutils.system" local log = require "lib/dtutils.log" local debug = require "darktable.debug" -local gettext = dt.gettext - - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("script_manager",dt.configuration.config_dir.."/lua/locale/") +local gettext = dt.gettext.gettext local function _(msgid) - return gettext.dgettext("script_manager", 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.error +local DEFAULT_LOG_LEVEL = log.warn -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 --- - - - - - - - - - - - - - - - - - - - - - - - --- P R E F E R E N C E S --- - - - - - - - - - - - - - - - - - - - - - - - - -dt.preferences.register(MODULE, "check_update", "bool", - "check for updated scripts on start up", - "automatically update scripts to correct version", - true) - -local check_for_updates = dt.preferences.read(MODULE, "check_update", "bool") +-- 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 -- - - - - - - - - - - - - - - - - - - - - - - - dt.preferences.register(MODULE, "check_update", "bool", - "check for updated scripts on start up", - "automatically update scripts to correct version", + _("check for updated scripts on start up"), + _("automatically update scripts to correct version"), true) local check_for_updates = dt.preferences.read(MODULE, "check_update", "bool") @@ -139,19 +131,24 @@ 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 + +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| @@ -159,7 +156,7 @@ sm.categories = {} 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 @@ -170,6 +167,8 @@ sm.categories = {} storage_name name of the exporter (in the exporter storage menu) has_action true if it creates an action action_name name on the button + has_select true if it creates a select + select_name name on the button has_event true if it creates an event handler event_type type of event, shortcut, post-xxx, pre-xxx callback name of the callback routine @@ -179,14 +178,12 @@ sm.categories = {} ]] 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 = { @@ -201,35 +198,70 @@ sm.run = false -- 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 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) - if not string.match(pref_type, "bool") then - log.msg(log.debug, "read value " .. tostring(val)) - end + + 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) - dt.preferences.write(MODULE, 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 +---------------- -- git interface +---------------- 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 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() @@ -237,21 +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() @@ -266,11 +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 @@ -278,34 +321,56 @@ 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 +-------------------- +-- utility functions +-------------------- + 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 combobox.value = selected + restore_log_level(old_log_level) end local function string_trim(str) - local result = string.gsub(str, "^%s+", "") - result = string.gsub(result, "%s+$", "") + local old_log_level = set_log_level(sm.log_level) + + 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 @@ -313,15 +378,112 @@ local function string_dequote(str) return string.gsub(str, "['\"]", "") end -local function add_script_category(category) - 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) +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 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) + -- 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) + local old_log_level = set_log_level(sm.log_level) local description = nil f = io.open(LUA_DIR .. PS .. script .. ".lua") if f then @@ -331,61 +493,84 @@ 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 - return "No documentation available" + restore_log_level(old_log_level) + 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, _("Loaded ") .. 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 - if script.data.destroy_method and script.data.destroy_method == "hide" then + if script.data.destroy_method and script.data.destroy_method == "hide" and script.data.show and dt.gui.current_view().id == "lighttable" then script.data.show() end else script.data = nil end + else - log.msg(log.screen, script.script_name .. _(" failed to load")) - 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 local function deactivate(script) - -- presently the lua api doesn't support unloading gui elements however, we - -- can hide libs, so we just mark those as hidden and hide the gui - -- can delete storage - --therefore we just mark then inactive for the next time darktable starts + -- presently the lua api doesn't support unloading lib elements however, we + -- can hide libs, so we just mark those as hidden and hide the gui + -- can delete storages + -- can delete actions + -- can delete selects + -- and mark them inactive for the next time darktable starts -- 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" @@ -397,38 +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, script.name .. _(" stopped")) + 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, script.name .. _(" will not start when darktable is restarted")) + 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) - log.msg(log.debug, "category is " .. 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, "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 @@ -445,27 +664,63 @@ 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) - log.msg(log.debug, "category is " .. category) - log.msg(log.debug, "name is " .. name) + local folder,path,name,filename,filetype = string.match(script_file, pattern) - add_script_category(category) + if folder and name and path then + log.msg(log.debug, "folder is " .. folder) + log.msg(log.debug, "name is " .. name) - if name then - add_script_name(name, path, category) + add_script_folder(folder) + add_script_name(name, path, folder) end + + restore_log_level(old_log_level) +end + +local function ensure_lib_in_search_path(line) + local old_log_level = set_log_level(sm.log_level) + + log.msg(log.debug, "line is " .. line) + + 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 + + local pattern = dt.configuration.running_os == "windows" and "(.+)\\lib\\.+lua" or "(.+)/lib/.+lua" + local path = string.match(line, pattern) + + log.msg(log.debug, "extracted path is " .. path) + log.msg(log.debug, "package.path is " .. package.path) + + if not string.match(package.path, ds.sanitize_lua(path)) then + + log.msg(log.debug, "path isn't in package.path, adding...") + + package.path = package.path .. ";" .. path .. "/?.lua" + + log.msg(log.debug, "new package.path is " .. package.path) + end + + restore_log_level(old_log_level) 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" + find_cmd = "dir /b/s \"" .. script_dir .. "\\*.lua\" | sort" end - log.msg(log.debug, _("find command is ") .. find_cmd) + + 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 @@ -475,20 +730,25 @@ local function scan_scripts(script_dir) process_script_data(script_file) script_count = script_count + 1 end + else + ensure_lib_in_search_path(line) -- but let's make sure libraries can be found end end end end + + restore_log_level(old_log_level) return script_count end local function update_scripts() + local old_log_level = set_log_level(sm.log_level) local result = false 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 @@ -502,68 +762,99 @@ 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) return result end +-------------- +-- UI handling +-------------- + 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) + + 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, _("category ") .. category .. _(" is already in use. Please specify a different category name.")) - 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 @@ -572,11 +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 @@ -588,126 +880,137 @@ 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(_("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) return result 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) - log.msg(log.debug, "looking for script " .. name .. " in category " .. category) - for _, script in ipairs(sm.scripts[category]) do +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 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) - log.msg(log.debug, "category is " .. category .. " and first is " .. first .. " and last is " .. last) +local function populate_buttons(folder, first, last) + local old_log_level = set_log_level(sm.log_level) + + 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 = script.name .. _(" started") - end + button.name = "pb_on" else - if sm.use_color then - button.label = script.name - button.name = "sm_stopped" - else - button.label = script.name .. _(" stopped") - 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 = script.name .. _(" stopped") - 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 = script.name .. " started" - 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 category = sm.page_status.category - log.msg(log.debug, "category is " .. category) - local num_scripts = #sm.scripts[category] + local old_log_level = set_log_level(sm.log_level) + + 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) @@ -729,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 @@ -745,136 +1050,189 @@ 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 = _("Page ") .. cur_page .. _(" of ") .. max_pages - populate_buttons(category, first, last) + sm.widgets.page_status.label = string.format(_("page %d of %d"), cur_page, max_pages) + + populate_buttons(folder, first, last) + + restore_log_level(old_log_level) end -local function change_category(category) - 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 +local function change_folder(folder) + local old_log_level = set_log_level(sm.log_level) + + 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", 600}}, -- containers + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_BOTTOM", 100}}, -- containers sm.widgets.main_box, nil,-- view_enter nil -- view_leave ) 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 -- - - - - - - - - - - - - - - - - - - - - - - - @@ -885,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 @@ -916,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") @@ -924,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 @@ -984,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) @@ -1005,63 +1379,87 @@ sm.widgets.add_scripts = dt.new_widget("box"){ } sm.widgets.allow_disable = dt.new_widget("check_button"){ - label = _('Enable "Disable Scripts" button'), + label = _('enable "disable scripts" button'), value = false, clicked_callback = function(this) if this.value == true then sm.widgets.disable_scripts.sensitive = true + else + sm.widgets.disable_scripts.sensitive = false end end, } sm.widgets.disable_scripts = dt.new_widget("button"){ - label = _("Disable Scripts"), + label = _("disable scripts"), sensitive = false, clicked_callback = function(this) 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) @@ -1089,10 +1487,12 @@ sm.widgets.page_control = dt.new_widget("box"){ sm.widgets.scripts = dt.new_widget("box"){ orientation = vertical, - dt.new_widget("label"){label = _("Scripts")}, - sm.widgets.category_selector, + dt.new_widget("section_label"){label = " "}, + dt.new_widget("label"){label = " "}, + dt.new_widget("label"){label = _("scripts")}, + sm.widgets.folder_selector, sm.widgets.page_control, - table.unpack(sm.widgets.buttons) + table.unpack(sm.widgets.boxes), } -- configure options @@ -1118,21 +1518,16 @@ sm.widgets.change_buttons = dt.new_widget("button"){ sm.widgets.configure = dt.new_widget("box"){ orientation = "vertical", - dt.new_widget("label"){label = _("Configuration")}, + 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"){ @@ -1164,6 +1559,7 @@ sm.widgets.main_box = dt.new_widget("box"){ } script_manager_running_script = nil + -- - - - - - - - - - - - - - - - - - - - - - - - -- D A R K T A B L E I N T E G R A T I O N -- - - - - - - - - - - - - - - - - - - - - - - - @@ -1177,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