2 Commits

Author SHA1 Message Date
Bill Ferguson
6548a12bf1 contrib/dbmaint - backported to API-9.2.0. Script to perform database
maintenance by removing database entries for missing
                  filmrolls and images.
2024-06-09 12:21:56 -04:00
Bill Ferguson
becfa5350b tools/script_manager - back ported gettext.bindomain fix 2024-05-18 09:56:50 -04:00
110 changed files with 2795 additions and 12967 deletions

View File

@@ -1,78 +1,5 @@
## 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

149
README.md
View File

@@ -15,17 +15,17 @@ These scripts are written primarily by the darktable developers and maintained b
Name|Standalone|OS |Purpose
----|:--------:|:---:|-------
[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
[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
### Contributed Scripts
@@ -34,52 +34,43 @@ These scripts are contributed by users. They are meant to have an "owner", i.e.
Name|Standalone|OS |Purpose
----|:--------:|:---:|-------
[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
[auto_tag_modified](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/auto_tag_modified)|Yes|LMW|Automatically tag images which have been manually modified in the darkroom
[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
change_group_leader|Yes|LMW|Change which image leads group
[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
website_gallery_export|No|LMW|Export a website gallery for selected images
[clear_GPS](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/clear_gps)|Yes|LMW|Reset GPS information for selected images
[CollectHelper](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/collecthelper)|Yes|LMW|Add buttons to selected images module to manipulate the collection
[copy_attach_detach_tags](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/copy_attach_detach_tags)|Yes|LMW|Copy and paste tags from/to images
[cr2hdr](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/cr2hdr)|Yes|L|Process image created with Magic Lantern Dual ISO
[enfuseAdvanced](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/enfuseadvanced)|No|LMW|Merge multiple images into Dynamic Range Increase (DRI) or Depth From Focus (DFF) images
[exportLUT](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/exportlut)|Yes|LMW|Create a LUT from a style and export it
[ext_editor](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/ext_editor)|No|LW|Export pictures to collection and edit them with up to nine user-defined external editors
[face_recognition](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/face_recognition)|No|LM|Identify and tag images using facial recognition
[fujifilm_dynamic_range](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/fujifilm_dynamic_range)|No|LMW|Correct fujifilm exposure based on exposure bias camera setting
[fujifilm_ratings](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/fujifilm_ratings)|No|LM|Support importing Fujifilm ratings
[geoJSON_export](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/geojson_export)|No|L|Create a geo JSON script with thumbnails for use in ...
[geoToolbox](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/geotoolbox)|No|LMW|A toolbox of geo functions
[gimp](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/gimp)|No|LMW|Open an image in GIMP for editing and return the result
[gpx_export](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/gpx_export)|No|LMW|Export a GPX track file from selected images GPS data
[HDRMerge](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/hdrmerge)|No|LMW|Combine the selected images into an HDR DNG and return the result
[hugin](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/hugin)|No|LMW|Combine selected images into a panorama and return the result
[image_stack](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/image_stack)|No|LMW|Combine a stack of images to remove noise or transient objects
[image_time](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/image_time)|Yes|LMW|Adjust the EXIF image time
[kml_export](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/kml_export)|No|L|Export photos with a KML file for usage in Google Earth
[LabelsToTags](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/labelstotags)|Yes|LMW|Apply tags based on color labels and ratings
[OpenInExplorer](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/openinexplorer)|No|LMW|Open the selected images in the system file manager
[passport_guide](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/passport_guide)|Yes|LMW|Add passport cropping guide to darkroom crop tool
[pdf_slideshow](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/pdf_slideshow)|No|LM|Export images to a PDF slideshow
[photils](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/photils)|No|LM|Automatic tag suggestions for your images
[quicktag](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/quicktag)|Yes|LMW|Create shortcuts for quickly applying tags
[rate_group](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/rate_group)|Yes|LMW|Apply or remove a star rating from grouped images
[rename_images](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/rename_images)|Yes|LMW|Rename single or multiple images
[rename-tags](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/rename-tags)|Yes|LMW|Change a tag name
[RL_out_sharp](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/rl_out_sharp)|No|LW|Output sharpening using GMic (Richardson-Lucy algorithm)
[select_non_existing](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/select_non_existing)|Yes|LMW|Enable selection of non-existing images in the the currently worked on images
[select_untagged](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/select_untagged)|Yes|LMW|Enable selection of untagged images
[slideshowMusic](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/slideshowMusic)|No|L|Play music during a slideshow
[transfer_hierarchy](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/transfer_hierarchy)|Yes|LMW|Image move/copy preserving directory hierarchy
[video_ffmpeg](https://darktable-org.github.io/luadocs/lua.scripts.manual/scripts/contrib/video_ffmpeg)|No|LMW|Export video from darktable
### Example Scripts
@@ -87,19 +78,17 @@ These scripts provide examples of how to use specific portions of the API. They
Name|Standalone|OS |Purpose
----|:--------:|:---:|-------
[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
[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
### Tools
@@ -107,11 +96,11 @@ Tool scripts perform functions relating to the repository, such as generating do
Name|Standalone|OS |Purpose
----|:--------:|:---:|-------
[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
[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
### Related third-party projects
@@ -129,8 +118,6 @@ 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
@@ -180,7 +167,7 @@ If you don't have %LOCALAPPDATA%\darktable you have to start dartable at least o
When darktable starts it looks for a file name `~/.config/darktable/luarc` (`%LOCALAPPDATA%\darktable\luarc` for windows) and reads it to see which scripts to include. The file is a plain text file with entries of the form `require "<directory>/<name>"` where directory is the directory containing the scripts, from the above list, and name is the name from the above list. To include GIMP the line would be `require "contrib/gimp"`.
The recommended way to enable and disable specific scripts is using the [script manager](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/tools/script_manager/) module. To use script manager do the following:
The recommended way to enable and disable specific scripts is using the script manager module. To use script manager do the following:
### Linux or MacOS
@@ -239,18 +226,18 @@ To update the script repository, open a terminal or command prompt and do the fo
## Documentation
The [Lua Scripts Manual](https://docs.darktable.org/lua/stable/lua.scripts.manual/) provides documentation
The [Lua Scripts Manual](https://darktable-org.github.io/luadocs/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://docs.darktable.org/lua/stable/lua.scripts.api.manual/) provides
The [Lua Scripts Library API Manual](https://darktable-org.github.io/luadocs/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://docs.darktable.org/lua/stable/lua.api.manual/) provides docuemntation of the
The [Lua API Manual](https://darktable-org.github.io/luadocs/lua.api.manual/) provides docuemntation of the
darktable Lua API.
## Troubleshooting

View File

@@ -1,225 +1,219 @@
--[[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 <http://www.gnu.org/licenses/>.
]]
--[[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
--[[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 <http://www.gnu.org/licenses/>.
]]
--[[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
script_data.show = nil -- only required for libs since the destroy_method only hides them
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
return script_data

View File

@@ -1,258 +1,254 @@
--[[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 <http://www.gnu.org/licenses/>.
]]
--[[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
--[[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 <http://www.gnu.org/licenses/>.
]]
--[[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
script_data.show = nil -- only required for libs since the destroy_method only hides them
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

View File

@@ -1,486 +1,479 @@
--[[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 <http://www.gnu.org/licenses/>.
]]
--[[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
--[[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 <http://www.gnu.org/licenses/>.
]]
--[[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
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
-- 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
return script_data

View File

@@ -52,23 +52,10 @@ 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
@@ -81,7 +68,7 @@ local ltt = {}
ltt.module_installed = false
ltt.event_registered = false
local LIB_ID = _("LabelsToTags")
local LIB_ID = "LabelsToTags"
-- Helper functions: BEGIN
@@ -122,23 +109,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 = {}
@@ -153,14 +140,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)
@@ -183,7 +170,7 @@ local mappingComboBox = darktable.new_widget("combobox"){
}
local function doTagging(selfC)
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)
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)
job.percent = 0.0
local pctIncrement = 1.0 / #(darktable.gui.action_images)
@@ -214,8 +201,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
}
}
@@ -245,7 +232,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

View File

@@ -1,261 +1,238 @@
--[[
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 <http://www.gnu.org/licenses/>.
]]
--[[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
--[[
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 <http://www.gnu.org/licenses/>.
]]
--[[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
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("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

View File

@@ -65,24 +65,10 @@ 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 <marco.carrarini@gmail.com>",
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
@@ -91,24 +77,20 @@ script_data.show = nil -- only required for libs since the destroy_method only h
-- 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
-- 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_NAME .. " exiftool not found, metadata not preserved")
end
end
end
-- setup export ---------------------------------------------------------------
@@ -155,7 +137,7 @@ local function export2RL(storage, image_table, extra_data)
for image, temp_name in pairs(image_table) do
i = i + 1
dt.print(string.format(_("sharpening image %d ..."), i))
dt.print(_("sharpening image ")..i.." ...")
-- create unique filename
new_name = output_folder..PS..df.get_basename(temp_name)..".jpg"
new_name = df.create_unique_filename(new_name)
@@ -172,10 +154,7 @@ local function export2RL(storage, image_table, extra_data)
if result ~= 0 then
dt.print(_("sharpening error"))
return
end
-- copy metadata from input_file to output_file
preserve_metadata(input_file, output_file)
end
-- delete temp image
os.remove(temp_name)

View File

@@ -1,132 +0,0 @@
--[[
auto_snapshot.lua - automatically take a snapshot when an image is loaded in darkroom
Copyright (C) 2024 Bill Ferguson <wpferguson@gamil.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
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 <wpferguson@gamil.com>
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 <const> = "auto_snapshot"
local DEFAULT_LOG_LEVEL <const> = 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 <wpferguson@gamil.com>", -- 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

View File

@@ -1,129 +0,0 @@
--[[
auto_tag_modified.lua - automatically tag an image when it has been manually modified in darkroom
Copyright (C) 2025 Michael Reiger <michael@rauschpfeife.net>, modeled after auto_snapshot.lua by Bill Ferguson <wpferguson@gamil.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
auto_tag_modified -
automatically tag an image when it has been manually modified in the darkroom, that is if its history has changed at the time it is closed from the darkroom.
(This will not catch applying styles from the light table though.)
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
None
USAGE
* start the script from script_manager
* open an image in darkroom, and close it after making an operation that changes the history
BUGS, COMMENTS, SUGGESTIONS
Michael Reiger <michael@rauschpfeife.net>
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 <const> = "auto_tag_modified"
local DEFAULT_TAG_NAME <const> = "darktable manually modified"
local DEFAULT_LOG_LEVEL <const> = 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 tag modified"), -- name of script
purpose = _("automatically tag an image when it has been manually modified in darkroom"), -- purpose of script
author = "Michael Reiger <michael@rauschpfeife.net>", -- your name and optionally e-mail address
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/auto_tag_modified/" -- 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_tag_modified = {}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- P R E F E R E N C E S
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.preferences.register(MODULE, "auto_modified_tag_name", "string", "auto_tag_modified - " .. _("tag name to use for auto tagging modified images"),
_("tag name to use for auto tagging modified images"), DEFAULT_TAG_NAME)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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-history-changed")
end
script_data.destroy = destroy
-- - - - - - - - - - - - - - - - - - - - - - - -
-- E V E N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.register_event(MODULE, "darkroom-image-history-changed",
function(event, image)
local tag_name = dt.preferences.read(MODULE, "auto_modified_tag_name", "string")
local tag = dt.tags.create(tag_name)
dt.tags.attach(tag, image)
end
)
return script_data

View File

@@ -39,29 +39,13 @@ 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 <cousinmarc@gmail.com>",
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
@@ -103,62 +87,56 @@ end
local function autostyle_apply_one_image (image)
local pref = darktable.preferences.read("autostyle", "exif_tag", "string")
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+)")
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
-- 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(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))
-- 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
end
end
@@ -176,7 +154,7 @@ local function autostyle_apply(shortcut)
images_submitted = images_submitted + 1
images_processed = images_processed + autostyle_apply_one_image(image)
end
darktable.print(string.format(_("applied auto style to %d out of %d image(s)"), images_processed, images_submitted))
darktable.print("Applied auto style to " .. images_processed .. " out of " .. images_submitted .. " image(s)")
end
local function destroy()
@@ -186,15 +164,13 @@ 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",
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.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.register_event("autostyle", "post-import-image",
autostyle_apply_one_image_event)
script_data.destroy = destroy
return script_data
return script_data

View File

@@ -36,34 +36,30 @@ USAGE
local dt = require "darktable"
local du = require "lib/dtutils"
local gettext = dt.gettext.gettext
local gettext = dt.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 <wpferguson@gmail.com>",
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
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(MODULE, dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext(MODULE, msgid)
end
-- create a namespace to contain persistent data and widgets
local chg_grp_ldr = {}
chg_grp_ldr = {}
local cgl = chg_grp_ldr
@@ -80,7 +76,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
@@ -125,7 +121,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
@@ -165,7 +161,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
@@ -202,4 +198,4 @@ script_data.restart = restart
script_data.destroy_method = "hide"
script_data.show = restart
return script_data
return script_data

View File

@@ -39,33 +39,29 @@
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 <wpferguson@gmail.com>",
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
script_data.show = nil -- only required for libs since the destroy_method only hides them
local gettext = dt.gettext
-- 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
@@ -85,13 +81,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 from selected images")
_("Clear GPS data")
)
return script_data

View File

@@ -45,7 +45,6 @@
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")
@@ -53,10 +52,13 @@ 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.gettext
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 function _(msgid)
return gettext(msgid)
return gettext.dgettext("color_profile_manager", msgid)
end
-- - - - - - - - - - - - - - - - - - - - - - - -
@@ -117,13 +119,13 @@ end
local function add_profile(file, dir)
df.file_copy(file, dir)
dt.print(string.format(_("added color profile %s to %s"), file, dir))
dt.print(_("added color profile " .. file .. " to " .. dir))
dt.print_log("color profile " .. file .. " added to " .. dir)
end
local function remove_profile(file, dir)
os.remove(dir .. PS .. file)
dt.print(string.format(_("removed color profile %s from %s"), file, dir))
dt.print(_("removed color profile " .. file .. " from " .. dir))
dt.print_log("color profile " .. file .. " removed from " .. dir)
end
@@ -362,13 +364,6 @@ end
local script_data = {}
script_data.metadata = {
name = _("color profile manager"),
purpose = _("manage external darktable color profiles"),
author = "Bill Ferguson <wpferguson@gmail.com>",
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"

View File

@@ -40,30 +40,26 @@ local dt = require "darktable"
local du = require "lib/dtutils"
local debug = require "darktable.debug"
local gettext = dt.gettext.gettext
local gettext = dt.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
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("copy_attach_detach_tags",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("copy_attach_detach_tags", msgid)
end
local cadt = {}
cadt.module_installed = false
cadt.event_registered = false
@@ -103,7 +99,7 @@ local function mcopy_tags()
end
end
dt.print(_('image tags copied ...'))
dt.print(_('Image tags copied ...'))
--create UI tag list
local taglist = ""
@@ -125,7 +121,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
@@ -149,7 +145,7 @@ local function attach_tags()
end
end
end
dt.print(_('tags attached ...'))
dt.print(_('Tags attached ...'))
end
local function detach_tags()
@@ -165,13 +161,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()

View File

@@ -37,23 +37,10 @@ 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 <theato@ttill.de>",
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
@@ -100,7 +87,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
@@ -111,7 +98,7 @@ local function convert_images()
end
local success_count = 0
for _ in pairs(processed_files) do success_count = success_count + 1 end
darktable.print(string.format(_("dual ISO conversion successful on %d/%d images."), success_count, #queue))
darktable.print("Dual ISO conversion successful on " .. success_count .. "/" .. #queue .. " images.")
job.valid = false
processed_files = {}
queue = {}
@@ -135,13 +122,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\nwarning: 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. Warning: cr2hdr is quite slow even in figuring out on whether the file is Dual ISO or not.", false)
script_data.destroy = destroy

View File

@@ -1,6 +1,6 @@
--[[
cycle_group_leader.lua - change image group leader
cycle_group_leader.lua - change image grouip leader
Copyright (C) 2024 Bill Ferguson <wpferguson@gmail.com>
@@ -54,34 +54,30 @@ local MODULE = "cycle_group_leader"
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 <wpferguson@gmail.com>",
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
-- - - - - - - - - - - - - - - - - - - - - - - - - -
-- I 1 8 N
-- - - - - - - - - - - - - - - - - - - - - - - - - -
local gettext = dt.gettext
-- 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
-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
@@ -104,7 +100,7 @@ end
local function cycle_group_leader(image)
local group_images = image:get_group_members()
if #group_images < 2 then
hinter_msg(_("no images to cycle through in group"))
hinter_msg(_("No images to cycle to in group"))
return
else
local position = nil
@@ -151,7 +147,7 @@ dt.register_event(MODULE, "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"))
dt.print(_("No image selected. Please select an image and try again"))
else
cycle_group_leader(images[1])
end

View File

@@ -2,7 +2,7 @@
dbmaint.lua - perform database maintenance
Copyright (C) 2024 Bill Ferguson <wpferguson@gamil.com>.
Copyright (C) 2024 Bill Ferguson <wpferguson.com>.
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
@@ -31,7 +31,7 @@
* look at the results and choose to delete or not
BUGS, COMMENTS, SUGGESTIONS
Bill Ferguson <wpferguson@gamil.com>
Bill Ferguson <wpferguson.com>
CHANGES
]]
@@ -63,8 +63,10 @@ du.check_min_api_version("7.0.0", MODULE) -- choose the minimum version that c
local gettext = dt.gettext.gettext
dt.gettext.bindtextdomain(MODULE , dt.configuration.config_dir .. "/lua/locale/")
local function _(msgid)
return gettext(msgid)
return gettext(MODULE, msgid)
end
@@ -80,9 +82,9 @@ script_data.restart = nil -- how to restart the (lib) script after it'
script_data.show = nil -- only required for libs since the destroy_method only hides them
script_data.metadata = {
name = _("db maintenance"),
name = "dbmaint",
purpose = _("perform database maintenance"),
author = "Bill Ferguson <wpferguson@gamil.com>",
author = "Bill Ferguson <wpferguson.com>",
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/dbmaint/"
}
@@ -115,20 +117,6 @@ 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 = {}
@@ -142,21 +130,19 @@ local function scan_film_rolls()
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)
log.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")
log.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
@@ -209,23 +195,21 @@ dbmaint.list_widget = dt.new_widget("text_view"){
}
dbmaint.chooser = dt.new_widget("combobox"){
label = _("scan for"),
label = "scan for",
selected = 1,
_("film rolls"), _("images"),
"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"),
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
if dbmaint.chooser.value == "film rolls" then
found = scan_film_rolls()
if #found > 0 then
for _, film in ipairs(found) do
@@ -234,7 +218,7 @@ dbmaint.scan_button = dt.new_widget("button"){
end
end
else
log.msg(log.debug, "checking path " .. dt.collection[1].path .. " for missing files")
log.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
@@ -243,14 +227,10 @@ dbmaint.scan_button = dt.new_widget("button"){
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
@@ -258,11 +238,11 @@ dbmaint.scan_button = dt.new_widget("button"){
}
dbmaint.remove_button = dt.new_widget("button"){
label = _("remove"),
tooltip = _("remove missing film rolls/images"),
label = "remove",
tooltip = "remove missing film rolls/images",
sensitive = false,
clicked_callback = function(this)
if dbmaint.chooser.selected == 1 then -- film rolls
if dbmaint.chooser.value == "film rolls" then
remove_missing_film_rolls(dbmaint.found)
else
remove_missing_images(dbmaint.found)
@@ -278,7 +258,7 @@ dbmaint.remove_button = dt.new_widget("button"){
dbmaint.main_widget = dt.new_widget("box"){
orientation = "vertical",
dt.new_widget("section_label"){label = _("missing items")},
dt.new_widget("section_label"){label = "missing items"},
dbmaint.list_widget,
dt.new_widget("label"){label = ""},
dbmaint.chooser,

View File

@@ -67,29 +67,22 @@ 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
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
local gettext = dt.gettext
gettext.bindtextdomain('enfuseAdvanced',dt.configuration.config_dir..'/lua/locale/')
local function _(msgid)
return gettext.dgettext('enfuseAdvanced', msgid)
end
-- INITS --
local AIS = {
name = 'align_image_stack',
@@ -267,7 +260,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(string.format(_("issue with %s executable"), prog.name))
dt.print(_('issue with ')..prog.name.._(' executable'))
else
prog.bin = CleanSpaces(prog.bin)
end
@@ -288,13 +281,7 @@ local function GetArgsFromPreference(prog_table, prefix) --for each arg in a pro
local temp = dt.preferences.read(mod, prefix..argument, arg_data.style)
if arg_data.style == 'bool' and temp then
prog_table.arg_string = prog_table.arg_string..arg_data.text..' '
elseif arg_data.style == 'integer' then
prog_table.arg_string = prog_table.arg_string..arg_data.text..temp..' '
elseif arg_data.style == 'string' then
if dt.configuration.running_os == "windows" then
--escape % with %% as % means env var on windows and call will fail otherwise
temp = string.gsub(temp, "%%", "%%%%")
end
elseif arg_data.style == 'integer' or arg_data.style == 'string' then
prog_table.arg_string = prog_table.arg_string..arg_data.text..temp..' '
elseif arg_data.style == 'float' then
temp = string.sub(tostring(temp),1,3)
@@ -351,7 +338,7 @@ local function UpdateENFargs(image_table, prefix) --updates the Enfuse arguments
end
ENF.arg_string = ENF.arg_string..' --depth='..GUI.Target.depth.value..' '
if GUI.Target.format.value == 'tif' then ENF.arg_string = ENF.arg_string..'--compression='..GUI.Target.compression_level_tif.value..' '
elseif GUI.Target.format.value == 'jpg' then ENF.arg_string = ENF.arg_string..'--compression='..tostring(math.floor(GUI.Target.compression_level_jpg.value))..' '
elseif GUI.Target.format.value == 'jpg' then ENF.arg_string = ENF.arg_string..'--compression='..GUI.Target.compression_level_jpg.value..' '
end
if not GUI.Target.source_location.value then out_path = GUI.Target.output_directory.value end
out_name = smallest_name..'-'..largest_id
@@ -390,7 +377,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(string.format(_("saved to %s"), preset))
dt.print(_('saved to ')..preset)
end
local function LoadFromPreference(preset) --load values from the specified 'preset' into the GUI elements
@@ -406,7 +393,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(string.format(_("loaded from %s"), preset))
dt.print(_('loaded from ')..preset)
end
local function remove_temp_files(images_to_remove) --deletes all files specified by the input string
@@ -441,7 +428,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(string.format(_("export for image fusion %d / %d"), math.floor(number), math.floor(total)))
dt.print(_('export for image fusion ')..tostring(math.floor(number))..' / '..tostring(math.floor(total)))
end
local function main(storage, image_table, extra_data)
@@ -449,7 +436,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 correct'))
dt.print(_('installation error, please verify binary paths are proper'))
return
end
local images_to_remove = ''
@@ -469,8 +456,8 @@ local function main(storage, image_table, extra_data)
job.valid = false
if resp ~= 0 then
remove_temp_files(images_to_remove)
dt.print(string.format(_("%s failed"), AIS.name))
dt.print_error(AIS.name .. ' failed')
dt.print_error(AIS.name.._(' failed'))
dt.print(AIS.name.._(' failed'))
return
end
end
@@ -496,9 +483,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(string.format(_("%s failed"), ENF.name))
job.valid = false
dt.print_error(ENF.name.._(' failed'))
dt.print(ENF.name.._(' failed'))
return
end
@@ -743,7 +729,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
}
@@ -896,7 +882,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
}
@@ -930,8 +916,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)
@@ -952,16 +938,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, separated by commas'),
placeholder = 'Enter tags, seperated by commas',
editable = true
}
temp = dt.preferences.read(mod, 'active_current_preset_ind', 'integer')
@@ -1028,7 +1014,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 = 'align_image_stack ' .. _('binary path'),
title = 'AIS binary path',
value = temp,
tooltip = temp,
is_directory = false,
@@ -1036,7 +1022,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,
@@ -1044,7 +1030,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,
@@ -1172,4 +1158,4 @@ end
script_data.destroy = destroy
return script_data
return script_data

View File

@@ -33,28 +33,23 @@ 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
script_data.show = nil -- only required for libs since the destroy_method only hides them
local gettext = dt.gettext
gettext.bindtextdomain("exportLUT",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("exportLUT", msgid)
end
du.check_min_api_version("5.0.0", "exportLUT")
local eL = {}
@@ -69,13 +64,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 = _("choose the identity file"),
title = _("Identity_file_chooser"),
value = "",
is_directory = false
}
local export_chooser_button = dt.new_widget("file_chooser_button"){
title = _("choose the export location"),
title = _("Export_location_chooser"),
value = "",
is_directory = true
}
@@ -111,9 +106,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
@@ -132,9 +127,9 @@ local function export_luts()
io_lut:write_image(identity, output_path(style.name, job))
count = count + 1
job.percent = count / size
dt.print(string.format(_("exported: %s"), output_path(style.name, job)))
dt.print(_("Exported: ") .. output_path(style.name, job))
end
dt.print(_("done exporting haldCLUTs"))
dt.print(_("Done exporting haldCLUTs"))
job.valid = false
identity:reset()
end

View File

@@ -75,24 +75,10 @@ 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
@@ -108,6 +94,13 @@ 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
@@ -163,7 +156,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(string.format(_("%d editors configured"), n_entries)) end
if update_button_pressed then dt.print(n_entries.._(" editors configured")) end
end
@@ -230,17 +223,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(string.format(_("error copying file %s"), name))
dt.print(_("error copying file ")..name)
return
end
end
-- launch the external editor, check result, return if error
local run_cmd = bin.." "..df.sanitize_filename(new_name)
dt.print(string.format(_("launching %s..."), friendly_name))
dt.print(_("launching ")..friendly_name.."...")
local result = dtsys.external_command(run_cmd)
if result ~= 0 then
dt.print(string.format(_("error launching %s"), friendly_name))
dt.print(_("error launching ")..friendly_name)
return
end
@@ -333,7 +326,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(string.format(_("error moving file %s"), temp_name))
dt.print(_("error moving file ")..temp_name)
return
end
@@ -385,7 +378,7 @@ end
local function restart()
for i = 1, MAX_EDITORS do
dt.register_event(MODULE_NAME .. i, "shortcut",
program_shortcut, string.format(_("edit with program %02d"), i))
program_shortcut, _("edit with program ")..string.format("%02d", i))
end
dt.register_storage("exp2coll", _("collection"), nil, export2collection)
dt.gui.libs[MODULE_NAME].visible = true
@@ -483,11 +476,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",
string.format(_("executable for external editor %d"), i),
_("select executable for external editor") , _("(none)"))
_("executable for external editor ")..i,
_("select executable for external editor") , _("(None)"))
dt.preferences.register(MODULE_NAME, "program_name_"..i, "string",
string.format(_("name of external editor %d"), i),
_("name of external editor ")..i,
_("friendly name of external editor"), "")
end
dt.preferences.register(MODULE_NAME, "show_in_darkrooom", "bool",
@@ -498,7 +491,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, string.format(_("edit with program %02d"), i))
program_shortcut, _("edit with program ")..string.format("%02d", i))
end

View File

@@ -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.gettext
local gettext = dt.gettext
-- constants
@@ -54,21 +54,10 @@ 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
@@ -80,6 +69,13 @@ 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 = ""
@@ -139,7 +135,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
@@ -209,7 +205,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
@@ -261,7 +257,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)
@@ -269,9 +265,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
@@ -403,7 +399,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,
@@ -486,12 +482,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)

View File

@@ -60,26 +60,13 @@ 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 <dant@pnym.net>",
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
@@ -124,17 +111,6 @@ 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()

View File

@@ -26,61 +26,52 @@ Dependencies:
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
local dtsys = require "lib/dtutils.system"
local gettext = dt.gettext.gettext
local gettext = dt.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 <ben.mendis@gmail.com>",
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
script_data.show = nil -- only required for libs since the destroy_method only hides them
gettext.bindtextdomain("fujifilm_ratings", dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("fujifilm_ratings", msgid)
end
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_log(command)
dt.print_error(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_log("using JPEG rating: " .. jpeg_result)
dt.print_error(_("Using JPEG Rating: ") .. tostring(jpeg_result))
return
end
command = "exiftool -Rating " .. RAF_filename
dt.print_log(command)
dt.print_error(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_log("using RAF rating: " .. raf_result)
dt.print_error(_("Using RAF Rating: ") .. tostring(raf_result))
end
end

View File

@@ -35,31 +35,26 @@ USAGE
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
local dtsys = require "lib/dtutils.system"
local gettext = dt.gettext.gettext
local gettext = dt.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
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("geoJSON_export",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("geoJSON_export", msgid)
end
-- Sort a table
local function spairs(_table, order) -- Code copied from http://stackoverflow.com/questions/15706270/sort-a-table-in-lua
-- collect the keys
@@ -85,24 +80,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
@@ -293,7 +288,7 @@ local function create_geoJSON_file(storage, image_table, extra_data)
file:write(geoJSON_file)
file:close()
dt.print(string.format(_("%s file created in %s"), "geoJSON", exportDirectory))
dt.print("geoJSON file created in "..exportDirectory)
-- Open the file with the standard programm
if ( dt.preferences.read("geoJSON_export","OpengeoJSONFile","bool") == true ) then
@@ -313,20 +308,20 @@ end
dt.preferences.register("geoJSON_export",
"CreateMapBoxHTMLFile",
"bool",
"geoJSON export: ".._("Create an additional HTML file"),
_("creates an HTML file that loads the geoJSON file. (needs a MapBox key"),
_("geoJSON export: Create an additional HTML file"),
_("Creates a HTML file, that loads the geoJASON 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")
@@ -338,8 +333,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

View File

@@ -1,7 +1,6 @@
--[[
This file is part of darktable,
copyright (c) 2016 Tobias Jakobs
copyright (c) 2025 Balázs Dura-Kovács
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -29,31 +28,26 @@ require "geoToolbox"
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
local dtsys = require "lib/dtutils.system"
local gettext = dt.gettext.gettext
local gettext = dt.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
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("geoToolbox",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("geoToolbox", msgid)
end
local gT = {}
gT.module_installed = false
@@ -62,35 +56,23 @@ gT.event_registered = false
-- <GUI>
local labelDistance = dt.new_widget("label")
labelDistance.label = _("distance:")
labelDistance.label = _("Distance:")
local checkbox_copy_gps_lat = dt.new_widget("check_button")
local label_copy_gps_lat = dt.new_widget("check_button")
{
label = _("latitude:"),
value = true
}
local entry_gps_lat = dt.new_widget("entry")
{
tooltip = _("latitude (editable)"),
placeholder = _("latitude")
}
local checkbox_copy_gps_lon = dt.new_widget("check_button")
local label_copy_gps_lon = dt.new_widget("check_button")
{
label = _("longitude:"),
value = true
}
local entry_gps_lon = dt.new_widget("entry")
{
tooltip = _("longitude (editable)"),
placeholder = _("longitude")
}
local checkbox_copy_gps_ele = dt.new_widget("check_button")
local label_copy_gps_ele = dt.new_widget("check_button")
{
label = _("elevation:"),
value = true
}
local entry_gps_ele = dt.new_widget("entry")
{
tooltip = _("elevation (editable)"),
placeholder = _("elevation")
}
-- </GUI>
local function select_with_gps()
@@ -180,7 +162,7 @@ local function get_first_coordinate()
first_elevation = ''
first_image_date = 0
for jj,image in ipairs(sel_images) do
for _,image in ipairs(sel_images) do
if not image then
first_have_data = false
else
@@ -219,7 +201,7 @@ local function get_second_coordinate()
second_elevation = ''
second_image_date = 0
for jj,image in ipairs(sel_images) do
for _,image in ipairs(sel_images) do
if not image then
second_have_data = false
else
@@ -255,7 +237,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 jj,image in ipairs(sel_images) do
for _,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
@@ -302,25 +284,25 @@ local function copy_gps()
copy_gps_longitude = ''
copy_gps_elevation = ''
for jj,image in ipairs(sel_images) do
for _,image in ipairs(sel_images) do
if not image then
copy_gps_have_data = false
else
copy_gps_have_data = true
if (image.latitude and checkbox_copy_gps_lat.value) then
if (image.latitude and label_copy_gps_lat.value) then
copy_gps_latitude = image.latitude
end
if (image.longitude and checkbox_copy_gps_lon.value) then
if (image.longitude and label_copy_gps_lon.value) then
copy_gps_longitude = image.longitude
end
if (image.elevation and checkbox_copy_gps_ele.value) then
if (image.elevation and label_copy_gps_ele.value) then
copy_gps_elevation = image.elevation
end
end
entry_gps_lat.text = copy_gps_latitude
entry_gps_lon.text = copy_gps_longitude
entry_gps_ele.text = copy_gps_elevation
label_copy_gps_lat.label = _("latitude: ") .. copy_gps_latitude
label_copy_gps_lon.label = _("longitude: ") ..copy_gps_longitude
label_copy_gps_ele.label = _("elevation: ") .. copy_gps_elevation
return
end
@@ -329,15 +311,15 @@ end
local function paste_gps(image)
local sel_images = dt.gui.action_images
for jj,image in ipairs(sel_images) do
if (checkbox_copy_gps_lat.value) then
image.latitude = entry_gps_lat.text
for _,image in ipairs(sel_images) do
if (label_copy_gps_lat.value) then
image.latitude = copy_gps_latitude
end
if (checkbox_copy_gps_lon.value) then
image.longitude = entry_gps_lon.text
if (label_copy_gps_lon.value) then
image.longitude = copy_gps_longitude
end
if (checkbox_copy_gps_ele.value) then
image.elevation = entry_gps_ele.text
if (label_copy_gps_ele.value) then
image.elevation = copy_gps_elevation
end
end
end
@@ -356,7 +338,7 @@ local function open_location_in_gnome_maps()
local i = 0;
-- Use the first image with geo information
for jj,image in ipairs(sel_images) do
for _,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
@@ -383,12 +365,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
@@ -399,7 +381,7 @@ local function reverse_geocode()
local i = 0;
-- Use the first image with geo information
for jj,image in ipairs(sel_images) do
for _,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
@@ -474,7 +456,7 @@ local function calc_distance()
local sel_images = dt.gui.selection()
for jj,image in ipairs(sel_images) do
for _,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
@@ -506,12 +488,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()
@@ -534,12 +516,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;
@@ -558,7 +540,7 @@ local function altitude_profile()
local elevationAdd = 0;
local sel_images = dt.gui.action_images
for jj,image in ipairs(sel_images) do
for _,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
@@ -600,7 +582,7 @@ local function altitude_profile()
file = io.open(exportDirectory.."/"..exportFilename, "w")
file:write(csv_file)
file:close()
dt.print(string.format(_("file created in %s"), exportDirectory))
dt.print(_("File created in ")..exportDirectory)
end
@@ -608,7 +590,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
@@ -630,14 +612,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
@@ -660,41 +642,29 @@ 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
},
dt.new_widget("box"){
orientation = "horizontal",
checkbox_copy_gps_lat,
entry_gps_lat
},
dt.new_widget("box"){
orientation = "horizontal",
checkbox_copy_gps_lon,
entry_gps_lon
},
dt.new_widget("box"){
orientation = "horizontal",
checkbox_copy_gps_ele,
entry_gps_ele
},
label_copy_gps_lat,
label_copy_gps_lon,
label_copy_gps_ele,
dt.new_widget("button")
{
label = _("apply GPS data to image"),
tooltip = _("apply GPS data to image"),
label = _("paste GPS data"),
tooltip = _("Paste GPS data"),
clicked_callback = paste_gps
},
separator2,--------------------------------------------------------
@@ -724,14 +694,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,--------------------------------------------------------
@@ -741,7 +711,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
@@ -769,19 +739,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

View File

@@ -69,31 +69,27 @@ 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
local gettext = dt.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 <wpferguson@gmail.com>",
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
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("gimp",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("gimp", msgid)
end
local function group_if_not_member(img, new_img)
local image_table = img:get_group_members()
local is_member = false
@@ -113,7 +109,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
@@ -123,7 +119,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
@@ -146,7 +142,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
@@ -217,7 +213,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

View File

@@ -25,30 +25,26 @@ For each source folder, a separate <trk> 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.gettext
local gettext = dt.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
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("gpx_export",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("gpx_export", msgid)
end
local gpx = {}
gpx.module_installed = false
gpx.event_registered = false
@@ -136,11 +132,11 @@ local function create_gpx_file()
local file = io.open(path, "w")
if (file == nil) then
dt.print(string.format(_("invalid path: %s"), path))
dt.print(_("invalid path: ")..path)
else
file:write(gpx_file)
file:close()
dt.print(string.format(_("gpx file created: "), path))
dt.print(_("gpx file created: ")..path)
end
end
@@ -148,7 +144,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

View File

@@ -32,28 +32,17 @@ USAGE
local dt = require "darktable"
local du = require "lib/dtutils"
local gettext = dt.gettext.gettext
local gettext = dt.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(msgid)
return gettext.dgettext("harmonic_armature_guide", 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)
@@ -86,12 +75,4 @@ end,
function()
return dt.new_widget("label"){label = _("harmonic armature"), halign = "start"}
end
)
local function destroy()
-- nothing to destroy
end
script_data.destroy = destroy
return script_data
)

View File

@@ -58,34 +58,30 @@ local MODULE = "hif_group_leader"
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 <wpferguson@gmail.com>",
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
-- - - - - - - - - - - - - - - - - - - - - - - - - -
-- I 1 8 N
-- - - - - - - - - - - - - - - - - - - - - - - - - -
local gettext = dt.gettext
-- 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
-- - - - - - - - - - - - - - - - - - - - - - - -
-- P R E F E R E N C E S
@@ -186,7 +182,7 @@ dt.register_event(MODULE .. "_collect", "shortcut",
local images = dt.collection
make_existing_hif_group_leader(images)
end,
_("make hif group leader for collection")
_("Make hif group leader for collection")
)
dt.register_event(MODULE .. "_select", "shortcut",
@@ -194,7 +190,7 @@ dt.register_event(MODULE .. "_select", "shortcut",
local images = dt.gui.selection()
make_existing_hif_group_leader(images)
end,
_("make hif group leader for selection")
_("Make hif group leader for selection")
)
return script_data

View File

@@ -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.gettext
local gettext = dt.gettext
local namespace = 'module_hugin'
local user_pref_str = 'prefer_gui'
@@ -56,26 +56,22 @@ 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
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("hugin",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("hugin", msgid)
end
local function user_preference_changed(widget)
user_prefer_gui = widget.value
dt.preferences.write(namespace, user_pref_str, "bool", user_prefer_gui)
@@ -83,7 +79,7 @@ end
local function show_status(storage, image, format, filename,
number, total, high_quality, extra_data)
dt.print(string.format(_("exporting to hugin: %d / %d"), number, total))
dt.print("exporting to hugin: "..tostring(number).."/"..tostring(total))
end
local function create_panorama(storage, image_table, extra_data) --finalize
@@ -144,7 +140,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
@@ -205,7 +201,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(string.format(_("importing file %s"), dst_path))
dt.print(_("importing file "..dst_path))
dt.database.import(dst_path)
end
end
@@ -231,7 +227,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

View File

@@ -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.gettext
local gettext = dt.gettext
local job = nil
-- path separator constant
@@ -73,26 +73,22 @@ 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 <wpferguson@gmail.com>",
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
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_stack",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("image_stack", msgid)
end
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-- GUI definitions
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -139,19 +135,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 the first'),
tooltip = _('optimize radial distortion for all images, \nexcept for 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 the first. \nUseful for aligning focus stacks (DFF) with slightly \ndifferent magnification.'),
tooltip =_('optimize field of view for all images, except for 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 the first.'),
tooltip =_('optimize image center shift for all images, \nexcept for first.'),
}
local chkbtn_auto_crop = dt.new_widget("check_button"){
@@ -263,7 +259,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
@@ -456,7 +452,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
@@ -500,11 +496,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
@@ -524,13 +520,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
@@ -544,7 +540,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
@@ -558,7 +554,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
@@ -572,7 +568,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
@@ -584,7 +580,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
@@ -596,7 +592,7 @@ end
dt.preferences.register("align_image_stack", "align_use_gpu", -- name
"bool", -- type
_('align image stack: use GPU for remapping'), -- label
_('align image stack: use GPU for remaping'), -- label
_('set the GPU remapping for image align'), -- tooltip
false)

View File

@@ -107,8 +107,7 @@ local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
local ds = require "lib/dtutils.string"
local dtsys = require "lib/dtutils.system"
local gettext = dt.gettext.gettext
local gettext = dt.gettext
local img_time = {}
img_time.module_installed = false
@@ -116,31 +115,25 @@ 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 <wpferguson@gmail.com>",
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 PS <const> = dt.configuration.runnin_os == "windows" and "\\" or "/"
local ERROR <const> = -1
local HAS_INTERNAL_METADATA = dt.metadata.exists("Exif.Image.DataTime")
local function _(msgid)
return gettext.dgettext("image_time", msgid)
end
local PS = dt.configuration.runnin_os == "windows" and "\\" or "/"
local ERROR = -1
-- function to convert from exif time to system time
local function exiftime2systime(exiftime)
@@ -155,12 +148,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 "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"
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 " "
return(y .. ":" .. mo .. ":" .. d .. " " .. h .. ":" .. m .. ":" .. s)
end
@@ -182,7 +175,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
@@ -194,7 +187,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)
@@ -212,6 +205,7 @@ local function year_month2months(year, month)
end
local function months2year_month(months)
dt.print_log("months is " .. months)
local year = math.floor(months / 12)
local month = months - (year * 12)
@@ -224,7 +218,7 @@ local function get_image_taken_time(image)
local exiv2 = df.check_if_bin_exists("exiv2")
if exiv2 then
p = io.popen(exiv2 .. " -K Exif.Image.DateTime " .. df.sanitize_filename(image.path .. PS .. image.filename))
p = io.popen(exiv2 .. " -K Exif.Image.DateTime " .. image.path .. PS .. image.filename)
if p then
for line in p:lines() do
if string.match(line, "Exif.Image.DateTime") then
@@ -242,37 +236,28 @@ end
local function _get_windows_image_file_creation_time(image)
local datetime = nil
local datestring = nil
if HAS_INTERNAL_METADATA then
datestring = image.DateString
else
local p = io.popen("dir " .. image.path .. PS .. image.filename)
if p then
for line in p:lines() do
if string.match(line, ds.sanitize_lua(image.filename)) then
datestring = line
local p = io.popen("dir " .. image.path .. PS .. image.filename)
if p then
for line in p:lines() do
if string.match(line, ds.sanitize_lua(image.filename)) then
local mo, day, yr, hr, min, apm = string.match(line, "(%d+)/(%d+)/(%d+) (%d-):(%d+) (%S+)")
if apm == "PM" then
hr = hr + 12
end
datetime = vars2exiftime(yr, mo, day, hr, min, 0)
end
p:close()
else
dt.print(string.format(_("unable to get information for %s"), image.filename))
datetime = ERROR
end
end
if datestring then
local mo, day, yr, hr, min, apm = string.match(datestring, "(%d+)/(%d+)/(%d+) (%d-):(%d+) (%S+)")
if apm == "PM" then
hr = hr + 12
end
datetime = vars2exiftime(yr, mo, day, hr, min, 0)
p:close()
else
dt.print(_("unable to get information for ") .. image.filename)
datetime = ERROR
end
return datetime
end
local function _get_nix_image_file_creation_time(image)
local datetime = nil
local p = io.popen("ls -lL --time-style=full-iso " .. df.sanitize_filename(image.path .. PS .. image.filename))
local p = io.popen("ls -lL --time-style=full-iso " .. image.path .. PS .. image.filename)
if p then
for line in p:lines() do
if string.match(line, ds.sanitize_lua(image.filename)) then
@@ -281,7 +266,7 @@ local function _get_nix_image_file_creation_time(image)
end
p:close()
else
dt.print(string.format(_("unable to get information for %s"), image.filename))
dt.print(_("unable to get information for ") .. image.filename)
datetime = ERROR
end
return datetime
@@ -329,7 +314,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
@@ -445,8 +430,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},
@@ -472,13 +457,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)
@@ -518,7 +503,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,

View File

@@ -58,34 +58,30 @@ local MODULE = "jpg_group_leader"
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 <wpferguson@gmail.com>",
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
-- - - - - - - - - - - - - - - - - - - - - - - - - -
-- I 1 8 N
-- - - - - - - - - - - - - - - - - - - - - - - - - -
local gettext = dt.gettext
-- 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
-- - - - - - - - - - - - - - - - - - - - - - - -
-- P R E F E R E N C E S
@@ -186,7 +182,7 @@ dt.register_event(MODULE .. "_collect", "shortcut",
local images = dt.collection
make_existing_jpg_group_leader(images)
end,
_("make jpg group leader for collection")
_("Make jpg group leader for collection")
)
dt.register_event(MODULE .. "_select", "shortcut",
@@ -194,7 +190,7 @@ dt.register_event(MODULE .. "_select", "shortcut",
local images = dt.gui.selection()
make_existing_jpg_group_leader(images)
end,
_("make jpg group leader for selection")
_("Make jpg group leader for selection")
)
return script_data

View File

@@ -39,34 +39,30 @@ local df = require "lib/dtutils.file"
local ds = require "lib/dtutils.string"
local dsys = require "lib/dtutils.system"
local gettext = dt.gettext.gettext
local gettext = dt.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
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("kml_export",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("kml_export", msgid)
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
-- Add duplicate index to filename
@@ -93,12 +89,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
@@ -108,7 +104,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
@@ -324,14 +320,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
@@ -350,23 +346,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 )
@@ -374,16 +370,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

View File

@@ -37,28 +37,17 @@ USAGE
local dt = require "darktable"
local du = require "lib/dtutils"
local gettext = dt.gettext.gettext
local gettext = dt.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(msgid)
return gettext.dgettext("passport_guide", 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)
@@ -102,13 +91,5 @@ 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

View File

@@ -40,29 +40,17 @@ USAGE
local dt = require "darktable"
local du = require "lib/dtutils"
local gettext = dt.gettext.gettext
local gettext = dt.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
gettext.bindtextdomain("passport_guide_germany",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext(msgid)
return gettext.dgettext("passport_guide_germany", 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)
@@ -113,13 +101,5 @@ function()
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

View File

@@ -42,14 +42,17 @@ local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
local gettext = dt.gettext.gettext
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 function _(msgid)
return gettext(msgid)
return gettext.dgettext("pdf_slideshow", 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
@@ -59,13 +62,6 @@ 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
@@ -235,7 +231,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
@@ -245,7 +241,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

View File

@@ -44,23 +44,10 @@ 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
@@ -68,6 +55,9 @@ script_data.show = nil -- only required for libs since the destroy_method only h
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
@@ -76,6 +66,10 @@ 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
@@ -184,7 +178,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()
@@ -241,7 +235,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]
@@ -253,7 +247,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
@@ -264,6 +258,10 @@ function PHOTILS.get_tags(image, with_export)
local out_arg = df.sanitize_filename(tmp_file)
local executable = photils_installed
if dt.configuration.running_os == "macos" then
executable = executable .. "/Contents/MacOS/photils-cli"
end
if with_export then
dt.print_log("use export to for prediction")
local export_file = df.create_tmp_file()
@@ -315,11 +313,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
@@ -334,7 +332,7 @@ function PHOTILS.on_tags_clicked()
end
if #PHOTILS.tags == 0 then
local msg = string.format(_("no tags were found"), MODULE_NAME)
local msg = string.format(_("no tags where found"), MODULE_NAME)
GUI.warning_label.label = msg
GUI.stack.active = GUI.error_view
return
@@ -389,7 +387,7 @@ end
local function install_module()
if not PHOTILS.module_installed then
dt.register_lib(MODULE_NAME,
_("photils auto-tagger"),
"photils autotagger",
true,
true,
PHOTILS.plugin_display_views,
@@ -430,9 +428,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") {
@@ -475,9 +473,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",

View File

@@ -49,23 +49,10 @@ 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
@@ -75,6 +62,15 @@ 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",
@@ -197,7 +193,7 @@ local function install_module()
if not qt.module_installed then
dt.register_lib(
"quicktag", -- Module name
_("quick tag"), -- name
"quicktag", -- name
true, -- expandable
false, -- resetable
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 490}},
@@ -304,7 +300,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(_("quick tag %i"),i))
string.format(_("quicktag %i"),i))
end
script_data.destroy = destroy

View File

@@ -44,23 +44,10 @@ 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
@@ -75,9 +62,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(string.format(_("applying rating %d to group(s)"), rating))
dt.print("applying rating " ..rating.. " to group(s)")
end
end
@@ -94,38 +81,37 @@ 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, string.format(_("rate group %d"), 0)
)
end, "Rate group 0")
dt.register_event("rg1", "shortcut",
function(event, shortcut)
apply_rating(1)
end, string.format(_("rate group %d"), 1))
end, "Rate group 1")
dt.register_event("rg2", "shortcut",
function(event, shortcut)
apply_rating(2)
end, string.format(_("rate group %d"), 2))
end, "Rate group 2")
dt.register_event("rg3", "shortcut",
function(event, shortcut)
apply_rating(3)
end, string.format(_("rate group %d"), 3))
end, "Rate group 3")
dt.register_event("rg4", "shortcut",
function(event, shortcut)
apply_rating(4)
end, string.format(_("rate group %d"), 4))
end, "Rate group 4")
dt.register_event("rg5", "shortcut",
function(event, shortcut)
apply_rating(5)
end, string.format(_("rate group %d"), 5))
end, "Rate group 5")
script_data.destroy = destroy

View File

@@ -36,23 +36,10 @@ 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
@@ -63,8 +50,8 @@ 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 = ''
@@ -75,11 +62,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
@@ -89,12 +76,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
@@ -117,7 +104,7 @@ local function rename_tags()
darktable.tags.delete (ot)
job.valid = false
darktable.print (string.format(_("renamed tags for %d images"), count))
darktable.print ("Renamed tags for " .. count .. " images")
old_tag.editable = true
new_tag.editable = true
@@ -128,7 +115,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
@@ -144,13 +131,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
}
@@ -159,7 +146,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

View File

@@ -43,19 +43,27 @@
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
local ds = require "lib/dtutils.string"
du.check_min_api_version("7.0.0", "rename_images")
local gettext = dt.gettext.gettext
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 function _(msgid)
return gettext(msgid)
return gettext.dgettext("rename_images", 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
@@ -64,13 +72,6 @@ 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 <wpferguson@gmail.com>",
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
@@ -85,13 +86,90 @@ 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
@@ -130,10 +208,9 @@ 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")
@@ -141,14 +218,14 @@ local function do_rename(images)
for i, image in ipairs(images) do
if job.valid then
job.percent = i / #images
ds.build_substitute_list(image, i, pattern, USER, PICTURES, HOME, DESKTOP)
local new_name = ds.substitute_list(pattern)
build_substitution_list(image, i, datetime, USER, PICTURES, HOME, DESKTOP)
local new_name = substitute_list(pattern)
if new_name == -1 then
dt.print(_("unable to do variable substitution, exiting..."))
stop_job(job)
return
end
ds.clear_substitute_list()
clear_substitute_list()
local args = {}
local path = string.sub(df.get_path(new_name), 1, -2)
if string.len(path) == 0 then
@@ -173,8 +250,7 @@ local function do_rename(images)
stop_job(job)
local collect_rules = dt.gui.libs.collect.filter()
dt.gui.libs.collect.filter(collect_rules)
dt.gui.views.lighttable.set_image_visible(first_image)
dt.print(string.format(_("renamed %d images"), #images))
dt.print(_("renamed " .. #images .. " images"))
else -- pattern length
dt.print_error("no pattern supplied, returning...")
dt.print(_("please enter the new name or pattern"))
@@ -185,19 +261,60 @@ local function do_rename(images)
end
end
local function reset_callback()
rename.widgets.pattern.text = ""
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- W I D G E T S
-- - - - - - - - - - - - - - - - - - - - - - - -
rename.widgets.pattern = dt.new_widget("entry"){
tooltip = ds.get_substitution_tooltip(),
placeholder = _("enter pattern") .. "$(FILE_FOLDER)/$(FILE_NAME)",
text = "",
reset_callback = function(self)
self.text = ""
dt.preferences.write(MODULE_NAME, "pattern", "string", self.text)
end
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)"),
text = ""
}
local pattern_pref = dt.preferences.read(MODULE_NAME, "pattern", "string")

View File

@@ -20,85 +20,46 @@ Enable selection of non-existing images in the the currently worked on images, e
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"
-- module name
local MODULE <const> = "select_non_existing"
local MODULE = "select_non_existing"
du.check_min_api_version("9.1.0", MODULE)
-- figure out the path separator
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
local DEFAULT_LOG_LEVEL <const> = log.info
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
log.log_level(DEFAULT_LOG_LEVEL)
local gettext = dt.gettext.gettext
local gettext = dt.gettext
gettext.bindtextdomain(MODULE, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS)
local function _(msgid)
return gettext(msgid)
return gettext.dgettext(MODULE, msgid)
end
local function stop_job(job)
job.valid = false
end
local function get_file_list(dir)
log.log_level(DEFAULT_LOG_LEVEL)
local file_list = {}
local cmd = nil
log.msg(log.debug, "called get_file_list with " .. dir)
if dt.configuration.running_os == "windows" then
cmd = "forfiles /P " .. ds.sanitize(dir) .. " /M * /C \"cmd /c echo @file\""
else
cmd = "cd " .. ds.sanitize(dir) ..";ls -1"
end
log.msg(log.info, "cmd is " .. cmd)
local p = io.popen(cmd)
if p then
for line in p:lines() do
if line:len() > 4 then
if not string.match(line, "xmp$") and not string.match(line, "XMP$") then
line = line:gsub("\"", "")
file_list[line] = true
end
end
end
p:close()
end
return file_list
end
local function select_nonexisting_images(event, images)
log.log_level(DEFAULT_LOG_LEVEL)
local selection = {}
local known_files = {}
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
if not known_files[image.path] then
known_files[image.path] = get_file_list(image.path)
end
if key % 10 == 0 then
job.percent = key/#images
end
local file_exists = known_files[image.path][image.filename]
log.msg(log.debug, image.path .. PS .. image.filename .." exists? => "..tostring(file_exists))
if (not file_exists) then
table.insert(selection, image)
end
else
break
end
end
stop_job(job)
return 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()
@@ -112,13 +73,5 @@ dt.gui.libs.select.register_selection(
_("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

View File

@@ -20,30 +20,26 @@ Enable selection of untagged images (darktable|* tags are ignored)
local dt = require "darktable"
local du = require "lib/dtutils"
local gettext = dt.gettext.gettext
local gettext = dt.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
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("select_untagged",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("select_untagged", msgid)
end
local function stop_job(job)
job.valid = false
end

View File

@@ -1,199 +0,0 @@
--[[
selected_image_visible.lua - make the selected image visible in lighttable
Copyright (C) 2026 Bill Ferguson <wpferguson@gmail.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
selected_image_visible - make the selected image visible in lighttable
selected_image_visible adds a bshortcut to make the currently
selected image or the first image of a selection visible in lighttable
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
None
USAGE
* enable the script in script_manager
BUGS, COMMENTS, SUGGESTIONS
Bill Ferguson <wpferguson@gmail.com>
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"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- A P I C H E C K
-- - - - - - - - - - - - - - - - - - - - - - - -
du.check_min_api_version("9.6.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 = _("selected image visible"), -- visible name of script
purpose = _("make the selected image visible in lighttable"), -- purpose of script
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/selected_image_visible/" -- URL to help/documentation
}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
local MODULE <const> = "selected_image_visible"
local DEFAULT_LOG_LEVEL <const> = log.info
local TMP_DIR <const> = dt.configuration.tmp_dir
-- path separator
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
-- command separator
local CS <const> = dt.configuration.running_os == "windows" and "&" or ";"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- L O G L E V E L
-- - - - - - - - - - - - - - - - - - - - - - - -
log.log_level(DEFAULT_LOG_LEVEL)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- N A M E S P A C E
-- - - - - - - - - - - - - - - - - - - - - - - -
local selected_image_visible = {}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- G L O B A L V A R I A B L E S
-- - - - - - - - - - - - - - - - - - - - - - - -
selected_image_visible.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 = selected_image_visible
-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
local function set_image_visible()
if dt.gui.current_view().name == "lighttable" and #dt.gui.action_images > 0 then
dt.gui.views.lighttable.set_image_visible(dt.gui.action_images[1])
end
end
local function set_button_sensitive()
if dt.gui.current_view().name == "lighttable"
and #dt.gui.selection() > 0
and not dt.gui.views.lighttable.is_image_visible(dt.gui.selection()[1]) then
dt.gui.libs.image.set_sensitive(MODULE, true)
else
dt.gui.libs.image.set_sensitive(MODULE, false)
end
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N P R O G R A M
-- - - - - - - - - - - - - - - - - - - - - - - -
-- - - - - - - - - - - - - - - - - - - - - - - -
-- U S E R I N T E R F A C E
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.gui.libs.image.register_action(
MODULE, _("selected image visible"),
function(event, images) set_image_visible() end,
_("make selected image visible in lighttable")
)
set_button_sensitive()
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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, "selection-changed")
dt.destroy_event(MODULE, "mouse-over-image-changed")
dt.gui.libs.image.destroy_action(MODULE)
end
script_data.destroy = destroy
-- - - - - - - - - - - - - - - - - - - - - - - -
-- E V E N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
if not dt.query_event(MODULE, "shortcut") then
dt.register_event(MODULE, "shortcut",
function(event, shortcut)
set_image_visible()
end, _("make selected image visible in lighttable")
)
end
dt.register_event(MODULE, "selection-changed",
function(event)
set_button_sensitive()
end
)
dt.register_event(MODULE, "mouse-over-image-changed",
function(event, image)
set_button_sensitive()
end
)
return script_data

View File

@@ -27,30 +27,26 @@ USAGE
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
local gettext = dt.gettext.gettext
local gettext = dt.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
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("slideshowMusic",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("slideshowMusic", msgid)
end
local function playSlideshowMusic(_, old_view, new_view)
local filename, playMusic
@@ -58,7 +54,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
@@ -74,7 +70,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
@@ -87,12 +83,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",

View File

@@ -1,313 +0,0 @@
--[[
toggle_group_view.lua - toggle only group visible in lighttable
Copyright (C) 2024 Bill Ferguson <wpferguson.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
toggle_group_view - toggle only group visible in lighttable
toggle_group_view provides a way to "separate" a group from
the lighttable display and limit the display to just the group.
Pressing the shortcut again returns to the full lighttable view.
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
None
USAGE
* place the script in your lua scripts folder
* start the script with script manager
* assign a key sequence to the shortcut
BUGS, COMMENTS, SUGGESTIONS
Bill Ferguson <wpferguson.com>
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 <const> = "toggle_group_view"
local DEFAULT_LOG_LEVEL <const> = log.error
local TMP_DIR <const> = dt.configuration.tmp_dir
-- path separator
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
-- command separator
local CS <const> = 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
dt.gettext.bindtextdomain(MODULE , dt.configuration.config_dir .. "/lua/locale/")
local function _(msgid)
return gettext(MODULE, 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 = "toggle_group_view", -- name of script
purpose = _("toggle only group visible in lighttable"), -- purpose of script
author = "Bill Ferguson <wpferguson.com>", -- your name and optionally e-mail address
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/toggle_group_view/" -- 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 toggle_group_view = {}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- G L O B A L V A R I A B L E S
-- - - - - - - - - - - - - - - - - - - - - - - -
local group_active = false
local filmroll = ""
local collection_rules = {}
local hover_active = true
-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
local function is_group_member(image)
if image then
if #image:get_group_members() > 1 then
return true
else
return false
end
end
end
local function is_group_displayed()
local rules = dt.gui.libs.collect.filter()
if rules[1].item == "DT_COLLECTION_PROP_GROUP_ID" then
local rule = dt.gui.libs.collect.new_rule()
rule.mode = "DT_LIB_COLLECT_MODE_AND"
rule.data = dt.collection[1].path
rule.item = "DT_COLLECTION_PROP_FILMROLL"
table.insert(collection_rules, rule)
group_active = true
end
end
local function set_button_sensitive(image)
local selected_images = dt.gui.selection()
local sensitive
if group_active then
dt.gui.libs.image.set_sensitive(MODULE, true)
return
end
if #selected_images > 0 then
if is_group_member(selected_images[1]) then
sensitive = true
end
else
if image then
if is_group_member(image) then
sensitive = true
else
sensitive = false
end
else
sensitive = false
end
end
dt.gui.libs.image.set_sensitive(MODULE, sensitive)
end
local function add_collection_rule(rule_string)
local rule = dt.gui.libs.collect.new_rule()
rule.mode = "DT_LIB_COLLECT_MODE_AND"
rule.data = rule_string
rule.item = "DT_COLLECTION_PROP_FILENAME"
table.insert(collection_rules, rule)
end
local function show_group(image)
local collection_rule_string = ""
local first = true
collection_rules = {}
local group_id = image.group_leader
local rule = dt.gui.libs.collect.new_rule()
rule.mode = "DT_LIB_COLLECT_MODE_AND"
rule.data = tostring(group_id.id)
rule.item = "DT_COLLECTION_PROP_GROUP_ID"
table.insert(collection_rules, rule)
-- dt.gui.libs.collect.filter returns the previous rule set when you install another
collection_rules = dt.gui.libs.collect.filter(collection_rules)
end
local function reset(image)
dt.gui.libs.collect.filter(collection_rules)
if image then
dt.gui.views.lighttable.set_image_visible(image)
else
log.msg(log.screen, _("no image supplied"))
end
set_button_sensitive(image)
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N
-- - - - - - - - - - - - - - - - - - - - - - - -
-- check if we are starting with a group already displayed and
-- configure appropriately
if not dt.preferences.read("darktable", "plugins/lighttable/act_on", "bool") then
hover_active = false
end
is_group_displayed()
-- - - - - - - - - - - - - - - - - - - - - - - -
-- U S E R I N T E R F A C E
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.gui.libs.image.register_action(
MODULE, "toggle group view",
function(event, images)
if group_active then
local image
if dt.gui.action_images[1] then
image = dt.gui.action_images[1]
reset(dt.gui.action_images[1])
else
image = dt.collection[1]
reset(dt.collection[1])
end
group_active = false
set_button_sensitive(image)
else
show_group(dt.gui.action_images[1])
group_active = true
end
end,
_("toggle group view for selected image")
)
set_button_sensitive(nil)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- D A R K T A B L E I N T E G R A T I O N
-- - - - - - - - - - - - - - - - - - - - - - - -
local function destroy()
if hover_active then
dt.destroy_event(MODULE, "mouse-over-image-changed")
end
dt.destroy_event(MODULE, "selection_changed")
dt.gui.libs.image.destroy_action(MODULE, "toggle group view")
-- leave the shortcut so it doesn't need to be reassigned
-- the next time this module starts
end
script_data.destroy = destroy
-- - - - - - - - - - - - - - - - - - - - - - - -
-- E V E N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.register_event(
MODULE, "shortcut",
function(event, shortcut)
if #dt.gui.action_images > 0 then
if group_active then
reset(dt.gui.action_images[1])
group_active = false
else
show_group(dt.gui.action_images[1])
group_active = true
end
end
end,
_("toggle group view for selected image")
)
---[[
if hover_active then
dt.register_event(MODULE, "mouse-over-image-changed",
function(event, image)
set_button_sensitive(image)
end
)
end
--]]
dt.register_event(MODULE, "selection-changed",
function(event)
set_button_sensitive(dt.gui.action_images[1])
end
)
return script_data

View File

@@ -76,26 +76,14 @@ 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
@@ -108,6 +96,12 @@ 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
@@ -144,7 +138,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
@@ -157,7 +151,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
@@ -270,21 +264,21 @@ local function doTransfer(transferFunc)
local sourceBase = sourceTextBox.text
local destBase = destinationTextBox.text
local destFilms = {}
for film, i in pairs(films) do
for film, _ in pairs(films) do
films[film] = destBase .. string.sub(film.path, #sourceBase+1)
if not pathExists(films[film]) then
if createDirectory(films[film]) == nil then
darktable.print(string.format(_("transfer hierarchy: ERROR: could not create directory: %s"), films[film]))
darktable.print(_("transfer hierarchy: ERROR: could not create directory: " .. films[film]))
return
end
end
if not pathIsDirectory(films[film]) then
darktable.print(string.format(_("transfer hierarchy: ERROR: not a directory: %s"), films[film]))
darktable.print(_("transfer hierarchy: ERROR: not a directory: " .. films[film]))
return
end
destFilms[film] = darktable.films.new(films[film])
if destFilms[film] == nil then
darktable.print(string.format(_("transfer hierarchy: ERROR: could not create film: %s"), film.path))
darktable.print(_("transfer hierarchy: ERROR: could not create film: " .. film.path))
end
end
@@ -293,7 +287,7 @@ local function doTransfer(transferFunc)
srcFilms[img] = img.film
end
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)
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)
job.percent = 0.0
local pctIncrement = 1.0 / #(darktable.gui.action_images)
for _,img in ipairs(darktable.gui.action_images) do
@@ -351,7 +345,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
}
}
@@ -405,4 +399,4 @@ script_data.restart = restart
script_data.destroy_method = "hide"
script_data.show = restart
return script_data
return script_data

File diff suppressed because it is too large Load Diff

View File

@@ -37,25 +37,14 @@ 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
local gettext = dt.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
@@ -64,6 +53,12 @@ script_data.show = nil -- only required for libs since the destroy_method only h
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
@@ -246,8 +241,8 @@ local function string_pref_write(name, widget_attribute)
end
local framerates_selector = dt.new_widget("combobox"){
label = _("frame rate"),
tooltip = _("select frame rate of output video"),
label = _("framerate"),
tooltip = _("select framerate of output video"),
value = combobox_pref_read("framerate", framerates),
changed_callback = combobox_pref_write("framerate"),
table.unpack(framerates)
@@ -299,7 +294,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),
@@ -380,7 +375,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(string.format(_("export %d / %d", number), total))
dt.print(_("export ")..tostring(number).." / "..tostring(total))
end
local function init_export(storage, img_format, images, high_quality, extra_data)
@@ -416,7 +411,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"]
@@ -448,7 +443,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"))
@@ -470,7 +465,7 @@ end
dt.register_storage(
"module_video_ffmpeg",
_("video ffmpeg"),
_(MODULE_NAME),
show_status,
finalize_export,
nil,

View File

@@ -1,294 +0,0 @@
--[[Export module to create a web gallery from selected images
copyright (c) 2025 Tino Mettler
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 this software. If not, see <http://www.gnu.org/licenses/>.
]]
--[[
TODO:
- Lua: remove images dir if already existent
- Lua: translations
]]
local dt = require "darktable"
local df = require "lib/dtutils.file"
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
local temp = dt.preferences.read('web_gallery', 'title', 'string')
if temp == nil then temp = 'Darktable gallery' end
local function _(msgid)
return dt.gettext.gettext(msgid)
end
local title_widget = dt.new_widget("entry")
{
text = temp
}
local temp = dt.preferences.read('web_gallery', 'destination_dir', 'string')
if temp == nil then temp = '' end
local dest_dir_widget = dt.new_widget("file_chooser_button")
{
title = _("select output folder"),
tooltip = _("select output folder"),
value = temp,
is_directory = true,
changed_callback = function(this) dt.preferences.write('web_gallery', 'destination_dir', 'string', this.value) end
}
local gallery_widget = dt.new_widget("box")
{
orientation=vertical,
dt.new_widget("label"){label = _("gallery title")},
title_widget,
dt.new_widget("label"){label = _("destination directory")},
dest_dir_widget
}
local function get_file_name(file)
return file:match("[^" .. PS .. "]*.$")
end
function escape_js_string(str)
local replacements = {
['\\'] = '\\\\',
['"'] = '\\"',
["'"] = "\\'",
['\n'] = '\\n',
['\r'] = '\\r',
['\t'] = '\\t',
['\b'] = '\\b',
['\f'] = '\\f',
['\v'] = '\\v'
}
return (str:gsub('[\\\"\n\r\t\b\f\v\']', replacements))
end
local function export_thumbnail(image, filename)
exporter = dt.new_format("jpeg")
exporter.quality = 90
exporter.max_height = 512
exporter.max_width = 512
exporter:write_image(image, filename, true)
end
local function write_image(image, dest_dir, filename)
df.file_move(filename, dest_dir.. PS .. "images" .. PS .. get_file_name(filename))
export_thumbnail(image, dest_dir .. PS .. "thumbnails" .. PS .. "thumb_" .. get_file_name(filename))
end
function exiftool_get_image_dimensions(filename)
local handle = io.popen("exiftool " .. filename)
local result = handle:read("*a")
handle:close()
for line in result:gmatch("[^\r\n]+") do
local w = line:match("^Image Width%s*:%s*(%d+)")
if w then
width = tonumber(w)
end
local h = line:match("^Image Height%s*:%s*(%d+)")
if h then
height = tonumber(h)
end
end
if width and height then
return width, height
else
return nil, nil
end
end
local function stop_job(job)
job.valid = false
end
local function fill_gallery_table(images_ordered, images_table, title, dest_dir, sizes, exiftool)
local gallery_data = { name = escape_js_string(title) }
local images = {}
local index = 1
local job = dt.gui.create_job(_("exporting thumbnail images"), true, stop_job)
for i, image in pairs(images_ordered) do
local filename = df.sanitize_filename(images_table[image])
dt.print(_("export thumbnail image ") .. index .. "/" .. #images_ordered)
write_image(image, dest_dir, filename)
if exiftool then
width, height = exiftool_get_image_dimensions(df.sanitize_filename(dest_dir .. PS .. "images" .. PS .. get_file_name(filename)))
else
width = sizes[index].width
height = sizes[index].height
end
local entry = { filename = "images" .. PS .. get_file_name(filename),
width = width, height = height }
images[index] = entry
job.percent = index / #images_ordered
index = index + 1
end
stop_job(job)
gallery_data.images = images
return gallery_data
end
local function generate_javascript_gallery_object(gallery)
local js = 'const gallery_data = {\n'
js = js .. ' name: "' .. gallery.name .. '",\n'
js = js .. ' images: [\n'
for i, img in ipairs(gallery.images) do
js = js .. string.format(' { filename: "%s",\n height: %d,\n width: %d }', img.filename, img.height, img.width)
if i < #gallery.images then
js = js .. ',\n'
else
js = js .. '\n'
end
end
js = js .. ' ]\n};\n'
return(js)
end
local function write_javascript_file(gallery_table, dest_dir)
dt.print(_("write JavaScript file"))
javascript_object = generate_javascript_gallery_object(gallery_table)
local fileOut, errr = io.open(dest_dir .. PS .. "js" .. PS .. "images.js", 'w+')
if fileOut then
fileOut:write(javascript_object)
else
log.msg(log.error, errr)
end
fileOut:close()
end
local function copy_static_files(dest_dir)
gfsrc = dt.configuration.config_dir .. PS .. "lua" .. PS .. "data" .. PS .. "website_gallery"
local gfiles = {
"index.html",
"css" .. PS .. "gallery.css",
"css" .. PS .. "modal.css",
"css" .. PS .. "style.css",
"js" .. PS .. "gallery.js",
"js" .. PS .. "modal.js",
"js" .. PS .. "fullscreen.js"
}
dt.print(_("copy static gallery files"))
for _, file in ipairs(gfiles) do
df.file_copy(gfsrc .. PS .. file, dest_dir .. PS .. file)
end
end
local function build_gallery(storage, images_table, extra_data)
local dest_dir = dest_dir_widget.value
df.mkdir(df.sanitize_filename(dest_dir))
df.mkdir(df.sanitize_filename(dest_dir .. PS .. "images"))
df.mkdir(df.sanitize_filename(dest_dir .. PS .. "thumbnails"))
df.mkdir(df.sanitize_filename(dest_dir .. PS .. "css"))
df.mkdir(df.sanitize_filename(dest_dir .. PS .. "js"))
local images_ordered = extra_data["images"] -- process images in the correct order
local sizes = extra_data["sizes"]
local title = _("Darktable export")
if title_widget.text ~= "" then
title = title_widget.text
end
local exiftool = df.check_if_bin_exists("exiftool");
gallerydata = fill_gallery_table(images_ordered, images_table, title, dest_dir, sizes, exiftool)
write_javascript_file(gallerydata, dest_dir)
copy_static_files(dest_dir)
end
local script_data = {}
script_data.metadata = {
name = _("website gallery (new)"),
purpose = _("create a web gallery from exported images"),
author = "Tino Mettler <tino+darktable@tikei.de>",
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/TODO"
}
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 destroy()
dt.preferences.write('web_gallery', 'title', 'string', title_widget.text)
dt.destroy_storage("module_webgallery")
end
script_data.destroy = destroy
local function show_status(storage, image, format, filename,
number, total, high_quality, extra_data)
dt.print(string.format(_("export image ").."%i/%i", number, total))
aspect = image.aspect_ratio
-- calculate the size of the exported image and store it in extra_data
-- to make it available in the finalize function
if image.final_height == 0 then
if aspect < 1 then
dimensions = { width = image.height, height = image.width }
else
dimensions = { width = image.width, height = image.height }
end
else
dimensions = { width = image.final_width, height = image.final_height }
end
if format.max_height > 0 and dimensions.height > format.max_height then
scale = format.max_height / dimensions.height
dimensions.height = math.floor(dimensions.height * scale + 0.5)
dimensions.width = math.floor(dimensions.width * scale + 0.5)
end
if format.max_width > 0 and dimensions.width > format.max_width then
scale = format.max_width / dimensions.width
dimensions.height = math.floor(dimensions.height * scale + 0.5)
dimensions.width = math.floor(dimensions.width * scale + 0.5)
end
extra_data["sizes"][number] = dimensions
end
local function initialize(storage, img_format, images, high_quality, extra_data)
dt.preferences.write('web_gallery', 'title', 'string', title_widget.text)
extra_data["images"] = images -- needed, to preserve images order
extra_data["sizes"] = {};
end
local supported_formats = { "jpg", "tif", "png", "webp" }
local formats_lut = {}
for key,format in pairs(supported_formats) do
formats_lut[format] = true
end
function check_supported(storage, format)
extension = format.extension
if formats_lut[extension] == true then
return true
else
return false
end
end
dt.register_storage("module_webgallery", "website gallery (new)", show_status, build_gallery, check_supported, initialize, gallery_widget)
return script_data

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -1,56 +0,0 @@
/*
copyright (c) 2025 Tino Mettler
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 this software. If not, see <http://www.gnu.org/licenses/>.
*/
body {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.heading h1 {
text-align: center;
display: grid;
}
.gallery {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.gallery img {
cursor: pointer;
object-fit: contain;
}
.thumb {
object-fit: contain;
flex-shrink: 0;
min-width: 100%;
min-height: 100%;
max-width: 100%;
max-height: 100%;
}
.thumb-box {
display: flex;
align-items: center;
object-fit: contain;
justify-content: center;
}

View File

@@ -1,99 +0,0 @@
/*
copyright (c) 2025 Tino Mettler
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 this software. If not, see <http://www.gnu.org/licenses/>.
*/
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.viewer {
position: relative;
width: 100vw;
height: 100vh;
touch-action: none;
overflow: hidden;
display: none;
}
.navgrid {
display: grid;
}
.nav {
height: var(--navigation-bar-height);
justify-content: center;
align-items: center;
display: flex;
z-index: 1001;
}
.slider {
height: calc(100vh - var(--navigation-bar-height));
position: relative;
width: 100vw;
touch-action: none;
overflow: hidden;
}
.slide-container {
position: absolute;
top: 0;
left: 0;
width: 300%;
height: 100%;
display: flex;
transform: translateX(-33.333%);
transition: transform 0.3s ease-out;
will-change: transform;
}
.slide {
width: 33.333%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: #000;
will-change: contents;
overflow: hidden;
position: relative;
touch-action: none;
object-fit: contain;
}
.slide img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
display: block;
transform-origin: 0 0;
cursor: zoom-in;
object-fit: contain;
}
.slide.zoomed img {
cursor: zoom-out;
max-width: none;
max-height: none;
}
.slide img.zooming {
transition: transform 0.2s ease-out;
}

View File

@@ -1,66 +0,0 @@
body {
font-family: system-ui, Arial, Helvetica, sans-serif;
background-color: rgb(238, 240, 242);
}
.heading h1 {
background-color: #cdd;
color: black;
}
.heading h1:after {
content: " ";
border-bottom: 1px solid #888888;
height: 1px;
}
.gallery {
gap: 3vw;
margin: 3vw auto;
padding: 1.5vw;
}
.gallery img {
background: none;
border: 1px outset #737780;
}
.gallery img:hover {
transform: scale(1.08);
}
.thumb-box {
background: none;
box-shadow: 0 1vw 4vw 1vw rgba(0,0,0,0.6);
}
.viewer {
--navigation-bar-height: 5vh;
}
.nav {
background-color: rgba(60,60,60, 0.4);
color: #ccc;
}
.navgrid {
grid-template-columns: 4em 1fr 1.5em 1.5em 1fr 1.5em 1.5em;
font-size: 3.5vh;
grid-column-gap: 1vw;
background-color: black;
}
.button {
cursor: pointer;
}
.button:hover {
cursor: pointer;
color: white;
text-shadow: 0px 0px 10px #ccc;
}
.counter {
font-size: 2.5vh;
}

View File

@@ -1,46 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0">
<link rel="stylesheet" href="css/modal.css">
<link rel="stylesheet" href="css/gallery.css">
<link rel="stylesheet" href="css/style.css">
<title id="page-title">Image Gallery</title>
</head>
<body>
<div id="heading1" class="heading">
<h1 id="gallery-title">Image Gallery</h1>
</div>
<div id="gallery" class="gallery"></div>
<div class="viewer" id="viewer">
<div class="navgrid">
<div id="counter" class="nav"></div>
<div class="spacer"></div>
<div class="button nav nav-arrow" id="prevArrow">&#9664;</div>
<div class="button nav nav-arrow" id="nextArrow">&#9654;</div>
<div class="spacer"></div>
<div id="fullscreen" class="button nav">&#x26F6;</div>
<div id="close" class="nav button">&times;</div>
</div>
<div class="slider" id="slider1">
<div class="slide-container">
<div class="slide" id="prev"></div>
<div class="slide" id="current"></div>
<div class="slide" id="next"></div>
</div>
</div>
</div>
<script type='text/javascript' >
var currentIndex = 1;
</script>
<script src="js/images.js"></script>
<script src="js/fullscreen.js"></script>
<script src="js/modal.js"></script>
<script src="js/gallery.js"></script>
</body>
</html>

View File

@@ -1,40 +0,0 @@
var isFullscreen = false;
var toggleFullscreen = function (ele) {
return isFullscreen ? exitFullscreen(ele) : requestFullscreen(ele);
};
var requestFullscreen = function (ele) {
if(isFullscreen == true) return 0 ;
isFullscreen = true;
if (ele.requestFullscreen) {
ele.requestFullscreen();
} else if (ele.webkitRequestFullscreen) {
ele.webkitRequestFullscreen();
} else if (ele.mozRequestFullScreen) {
ele.mozRequestFullScreen();
} else if (ele.msRequestFullscreen) {
ele.msRequestFullscreen();
} else {
console.log('Fullscreen API is not supported.');
}
};
var exitFullscreen = function () {
if(isFullscreen == false) return 0;
isFullscreen = false;
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else {
console.log('Fullscreen API is not supported.');
}
};

View File

@@ -1,112 +0,0 @@
/*
copyright (c) 2025 Tino Mettler
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 this software. If not, see <http://www.gnu.org/licenses/>.
*/
var scrollPosX = 0;
var scrollPosY = 0;
document.addEventListener('DOMContentLoaded', function () {
var imageCount = 0;
const gallery = document.getElementById('gallery');
const viewer = document.getElementById('viewer');
function showModal(e) {
const thumbbox = e.target.parentElement;
const index = [...gallery.children].indexOf(thumbbox);
currentIndex = index;
scrollPosX = document.documentElement.scrollLeft;
scrollPosY = document.documentElement.scrollTop;
gallery.style.display = 'none';
document.getElementById('heading1').style.display = 'none';
viewer.style.display = 'grid';
loadSlides();
updateCounter(currentIndex);
updateNavigationState();
}
function closeModal() {
exitFullscreen(document.documentElement);
viewer.style.display = 'none';
document.getElementById('heading1').style.display = 'grid';
gallery.style.display = 'flex';
document.documentElement.scrollTo({
left: scrollPosX,
top: scrollPosY,
behavior: "instant",
});
};
function createThumbnailElement(imageObj) {
const frame = document.createElement('div');
frame.className = 'thumb-box';
const framesize = 18;
const width = parseInt(imageObj.width);
const height = parseInt(imageObj.height);
const sum = width + height;
const scalefactor = sum / (framesize * 2.0);
frame.style.width = (width / scalefactor) + 'vw';
frame.style.height = (height / scalefactor) + 'vw';
const img = document.createElement('img');
img.className = 'thumb';
img.src = imageObj.filename.replace(/images\/(.*)$/i, 'thumbnails/thumb_$1');
img.alt = imageObj.filename;
img.addEventListener('click', function (e) { e.stopPropagation(); showModal(e); });
frame.appendChild(img);
gallery.appendChild(frame);
}
const images = gallery_data.images;
const title = document.getElementById('gallery-title');
const pageTitle = document.getElementById('page-title');
if (gallery_data.name) {
title.textContent = gallery_data.name;
pageTitle.textContent = gallery_data.name;
}
document.getElementById('close').onclick = function (e) {
e.stopPropagation();
closeModal();
};
// Keyboard navigation using left/right arrow keys
document.onkeyup = function (e) {
e.stopPropagation();
switch(e.key) {
case "Escape":
closeModal();
break;
}
};
document.getElementById('fullscreen').onclick = function (e) {
e.stopPropagation();
toggleFullscreen(document.documentElement);
};
// Populate thumbnail gallery
images.forEach(function (imageObj) {
createThumbnailElement(imageObj);
});
});

View File

@@ -1,355 +0,0 @@
/*
copyright (c) 2025 Tino Mettler
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 this software. If not, see <http://www.gnu.org/licenses/>.
*/
const container = document.querySelector('.slide-container');
const slides = {
prev: document.getElementById('prev'),
current: document.getElementById('current'),
next: document.getElementById('next')
};
const prevArrow = document.getElementById('prevArrow');
const nextArrow = document.getElementById('nextArrow');
let startX = 0;
let startY = 0;
let isDragging = false; // For swipe navigation
let isZoomed = false;
let isPanning = false; // Panning in zoomed state
let hasPanned = false;
let translateX = 0;
let translateY = 0;
let currentScale = 1;
let img;
let baseWidth, baseHeight; // Image dimensions before zoom
let scaledWidth, scaledHeight;
const images = gallery_data.images;
function updateCounter(index) {
const counter = document.getElementById('counter');
counter.textContent = (index + 1) + ' / ' + images.length;
}
function updateBoundaries() {
containerRect = container.getBoundingClientRect();
// Calculate the actual scaled dimensions based on the base (pre-zoom) size
scaledWidth = baseWidth * currentScale;
scaledHeight = baseHeight * currentScale;
}
function limitPanning(proposedX, proposedY) {
const maxX = 0;
const minX = baseWidth - scaledWidth;
const maxY = 0;
const minY = baseHeight - scaledHeight;
let constrainedX = proposedX;
let constrainedY = proposedY;
// Apply constraints
if (scaledWidth > baseWidth) {
// Image wider than original - constrain panning
constrainedX = Math.max(minX, Math.min(maxX, proposedX));
} else {
// Image narrower than original - center it
constrainedX = (baseWidth - scaledWidth) / 2;
}
if (scaledHeight > baseHeight) {
// Image taller than original - constrain panning
constrainedY = Math.max(minY, Math.min(maxY, proposedY));
} else {
// Image shorter than original - center it
constrainedY = (baseHeight - scaledHeight) / 2;
}
return {
x: constrainedX,
y: constrainedY
};
}
function handleZoom(e) {
// If we were actually panning, don't zoom
if (hasPanned) {
isPanning = false;
hasPanned = false;
return;
}
// If we're zoomed and haven't panned, zoom out
if (isZoomed) {
img.classList.add('zooming');
translateX = 0;
translateY = 0;
currentScale = 1;
img.style.transform = 'none';
container.classList.remove('zoomed');
isZoomed = false;
updateNavigationState();
setTimeout(() => {
img.classList.remove('zooming');
}, 200);
return;
}
// Zoom in at clicked/tapped point
if (!isZoomed) {
const rect = img.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Store the base (pre-zoom) dimensions and offset
baseWidth = rect.width;
baseHeight = rect.height;
// Calculate the scale for 1:1 pixel zoom
currentScale = img.naturalWidth / rect.width;
// Calculate the translation needed to keep clicked point under cursor
// After scaling, the point at (x, y) will be at (x * currentScale, y * currentScale)
// We want it to remain at (e.clientX - rect.left, e.clientY - rect.top)
translateX = e.clientX - rect.left - (x * currentScale);
translateY = e.clientY - rect.top - (y * currentScale);
updateBoundaries();
const limited = limitPanning(translateX, translateY);
translateX = limited.x;
translateY = limited.y;
img.classList.add('zooming');
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${currentScale})`;
container.classList.add('zoomed');
setTimeout(() => {
img.classList.remove('zooming');
}, 200);
}
isZoomed = true;
updateNavigationState();
}
function createImageElement(imageData) {
if (!imageData) return null;
const img = new Image();
img.src = imageData.filename;
img.width = imageData.width;
img.height = imageData.height;
img.addEventListener('dragstart', (e) => e.preventDefault());
return img;
}
function loadSlides() {
slides.prev.innerHTML = '';
slides.current.innerHTML = '';
slides.next.innerHTML = '';
if (currentIndex > 0) {
const prevImg = createImageElement(images[currentIndex - 1]);
if (prevImg) slides.prev.appendChild(prevImg);
}
const currentImg = createImageElement(images[currentIndex]);
if (currentImg) {
img = currentImg;
slides.current.appendChild(currentImg);
}
if (currentIndex < images.length - 1) {
const nextImg = createImageElement(images[currentIndex + 1]);
if (nextImg) slides.next.appendChild(nextImg);
}
updateCounter(currentIndex)
updateNavigationState();
}
function updateNavigationState() {
prevArrow.style.visibility = (currentIndex === 0 || isZoomed) ? 'hidden' : 'visible';
nextArrow.style.visibility = (currentIndex === images.length - 1 || isZoomed) ? 'hidden' : 'visible';
}
async function showPreviousImage() {
if (currentIndex > 0 && !isZoomed) {
currentIndex--;
container.style.transition = 'transform 0.3s ease-out';
container.style.transform = 'translateX(0%)';
await waitForTransition();
container.style.transition = 'none';
container.style.transform = 'translateX(-33.333%)';
loadSlides();
}
}
async function showNextImage() {
if (currentIndex < images.length - 1 && !isZoomed) {
currentIndex++;
container.style.transition = 'transform 0.3s ease-out';
container.style.transform = 'translateX(-66.666%)';
await waitForTransition();
container.style.transition = 'none';
container.style.transform = 'translateX(-33.333%)';
loadSlides();
}
}
function handleTouchStart(e) {
if (isZoomed) return;
startX = e.touches[0].clientX;
isDragging = true;
container.style.transition = 'none';
}
function handleTouchMove(e) {
if (!isDragging || isZoomed) return;
const currentX = e.touches[0].clientX;
const diff = currentX - startX;
const baseOffset = -33.333;
const percentMoved = (diff / window.innerWidth) * 33.333;
container.style.transform = `translateX(${baseOffset + percentMoved}%)`;
}
async function handleTouchEnd(e) {
if (!isDragging)
if(isZoomed) {
handleZoom(e);
return;
}
isDragging = false;
const endX = e.changedTouches[0].clientX;
const diff = endX - startX;
const threshold = window.innerWidth * 0.2;
container.style.transition = 'transform 0.3s ease-out';
if (diff > threshold && currentIndex > 0) {
await showPreviousImage();
} else if (diff < -threshold && currentIndex < images.length - 1) {
await showNextImage();
} else {
container.style.transform = 'translateX(-33.333%)';
}
}
function handleKeyDown(e) {
if (isZoomed) return;
if (e.code === 'Space' || e.code === 'ArrowRight') {
e.preventDefault();
showNextImage();
} else if (e.code === 'Backspace' || e.code === 'ArrowLeft') {
e.preventDefault();
showPreviousImage();
}
}
function waitForTransition() {
return new Promise(resolve => {
container.addEventListener('transitionend', resolve, { once: true });
});
}
// Initialize
loadSlides();
// Navigation event listeners
container.addEventListener('touchstart', handleTouchStart);
container.addEventListener('touchmove', handleTouchMove);
container.addEventListener('touchend', handleTouchEnd);
container.addEventListener('click', handleZoom);
document.addEventListener('keydown', handleKeyDown);
prevArrow.addEventListener('click', showPreviousImage);
nextArrow.addEventListener('click', showNextImage);
// Mouse panning event listeners
container.addEventListener('mousedown', function(e) {
if (isZoomed && e.target.tagName === 'IMG') {
isPanning = true;
hasPanned = false;
updateBoundaries();
startX = e.clientX - translateX;
startY = e.clientY - translateY;
e.preventDefault();
container.style.cursor = 'grabbing';
}
});
window.addEventListener('mousemove', function(e) {
if (isPanning && isZoomed) {
const proposedX = e.clientX - startX;
const proposedY = e.clientY - startY;
const limited = limitPanning(proposedX, proposedY);
translateX = limited.x;
translateY = limited.y;
const img = slides.current.querySelector('img');
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${currentScale})`;
hasPanned = true;
}
});
window.addEventListener('mouseup', function() {
if (isPanning) {
isPanning = false;
container.style.cursor = isZoomed ? 'zoom-out' : 'zoom-in';
}
});
// Touch panning event listeners
container.addEventListener('touchstart', function(e) {
if (isZoomed) {
isPanning = true;
hasPanned = false;
updateBoundaries();
const touch = e.touches[0];
startX = touch.clientX - translateX;
startY = touch.clientY - translateY;
e.preventDefault();
}
});
container.addEventListener('touchmove', function(e) {
if (isPanning && isZoomed) {
const touch = e.touches[0];
const proposedX = touch.clientX - startX;
const proposedY = touch.clientY - startY;
const limited = limitPanning(proposedX, proposedY);
translateX = limited.x;
translateY = limited.y;
const img = slides.current.querySelector('img');
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${currentScale})`;
hasPanned = true;
e.preventDefault();
}
});
container.addEventListener('touchend', function(e) {
if (isPanning) {
isPanning = false;
}
});

View File

@@ -1,323 +0,0 @@
--[[
This file is part of darktable,
copyright (c) 2026 darktable developers.
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 <http://www.gnu.org/licenses/>.
]]
--[[
ai_denoise - example script demonstrating the darktable.ai Lua
API by denoising the selected images with tile-based inference.
Adds an "AI denoise" panel to the lighttable right column with a
"denoise selected" button. Each selected image is loaded through
the develop pipeline (so denoise sees the output of all enabled
IOPs, not the original raw), run tile-by-tile through the active
denoise model, and saved as a 16-bit TIFF grouped with the source.
Demonstrates:
* model lookup via dt.ai.model_for_task
* tensor crop/paste for tile-based inference
* linear <-> sRGB conversion (NR models train on gamma input)
* edge-replicated padding to suppress border artifacts
* background job with progress bar and cancellation
LIMITATIONS
* Image must be at least TILE_SIZE x TILE_SIZE (768 x 768 for
denoise-nind / denoise-nafnet); smaller inputs return an error.
* If a save fails mid-batch the Lua error propagates and aborts
the whole job. Wrap denoise_one in pcall if you need
batch-robust behaviour.
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
* a denoise model enabled in preferences -> AI (denoise-nind,
denoise-nafnet, ...)
USAGE
* require this script from your luarc
* select one or more images in lighttable
* click the "denoise selected" button in the AI denoise panel
BUGS, COMMENTS, SUGGESTIONS
* file an issue on https://github.com/darktable-org/darktable
CHANGES
]]
local dt = require "darktable"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
local MODULE_NAME = "ai_denoise"
-- darktable's NR models use static-shape ONNX: each model has a fixed
-- input dimension baked in at training time. The Lua API doesn't
-- expose this, so keep TILE_SIZE in sync with the model's
-- `attributes.input_sizes` in its config.json.
local TILE_SIZE = 768 -- denoise-nind, denoise-nafnet
-- overlap between adjacent tiles. larger values produce smoother
-- seams at the cost of more compute per pixel
local OVERLAP = 64
-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
-- Surround the input with `pad` pixels on every side, replicating the
-- outermost rows/columns ("edge clamp" padding). Boundary tiles then
-- see plausible context instead of a literal image edge, so the
-- network's internal zero-padding no longer leaks into the visible
-- output as colour artifacts along the image rim.
local function pad_replicate(input, pad)
local C = input:shape()[2]
local H = input:shape()[3]
local W = input:shape()[4]
local padded = dt.ai.create_tensor({1, C, H + 2 * pad, W + 2 * pad})
-- center: original image
padded:paste(input, pad, pad)
-- left and right strips: replicate first / last column of input
local left_col = input:crop(0, 0, H, 1)
local right_col = input:crop(0, W - 1, H, 1)
for i = 0, pad - 1 do
padded:paste(left_col, pad, i)
padded:paste(right_col, pad, pad + W + i)
end
-- top and bottom strips: replicate first / last row of the already
-- horizontally-padded buffer so the four corners get filled correctly
local top_row = padded:crop(pad, 0, 1, W + 2 * pad)
local bottom_row = padded:crop(pad + H - 1, 0, 1, W + 2 * pad)
for i = 0, pad - 1 do
padded:paste(top_row, i, 0)
padded:paste(bottom_row, pad + H + i, 0)
end
return padded
end
-- Tile a [1,C,H,W] tensor through the model with overlap-blended seams.
-- Each tile is tile_size x tile_size, successive tiles step by
-- (tile_size - 2*overlap), and only the central (non-overlap) region
-- of each tile is pasted into the output. Image-boundary edges keep
-- their full extent; only image-interior seams drop the overlap margin.
--
-- The trailing tile in each axis is pinned to the image boundary so
-- the residual right/bottom strip is never smaller than 2*overlap --
-- otherwise it would have to be skipped (the model has a fixed input
-- size) and those pixels would never be written.
local function run_tiled(ctx, input, tile_size, overlap,
job, base_pct, span_pct)
local C = input:shape()[2]
local H = input:shape()[3]
local W = input:shape()[4]
if H < tile_size or W < tile_size then
return nil, string.format("image too small (%dx%d) for tile %d",
W, H, tile_size)
end
local step = tile_size - 2 * overlap
-- enumerate tile origins along one axis, ending exactly at the boundary
local function origins(extent)
local out, p = {}, 0
while p + tile_size < extent do
out[#out + 1] = p
p = p + step
end
out[#out + 1] = math.max(0, extent - tile_size)
return out
end
local ys, xs = origins(H), origins(W)
local total = #xs * #ys
local output = dt.ai.create_tensor({1, C, H, W})
local done = 0
for _, ty in ipairs(ys) do
for _, tx in ipairs(xs) do
if job and not job.valid then return nil, "cancelled" end
local tile_in = input:crop(ty, tx, tile_size, tile_size)
local tile_out = ctx:run(tile_in)
-- central region to keep: drop overlap on every inner edge;
-- full extent at image-boundary edges
local at_top = (ty == 0)
local at_left = (tx == 0)
local at_bottom = (ty + tile_size >= H)
local at_right = (tx + tile_size >= W)
local cy = at_top and 0 or overlap
local cx = at_left and 0 or overlap
local ch = tile_size - cy - (at_bottom and 0 or overlap)
local cw = tile_size - cx - (at_right and 0 or overlap)
output:paste(tile_out:crop(cy, cx, ch, cw), ty + cy, tx + cx)
done = done + 1
if job then
job.percent = base_pct + span_pct * (done / total)
end
end
end
return output
end
-- Denoise one image: load through the develop pipeline, run tiled
-- inference, save as 16-bit TIFF, group with the source.
local function denoise_one(ctx, img, tile_size, job, base_pct, span_pct)
-- darktable returns the develop-pipeline output as scene-linear RGB;
-- the NR models were trained on sRGB-gamma input, so we re-encode
-- before inference and decode back to linear before saving
local input = dt.ai.load_image(img)
if not input then return false, "load_image failed" end
input:linear_to_srgb()
-- pad so the image's outer rim is no longer at a tile boundary;
-- the padded strip absorbs the network's edge artifacts and gets
-- cropped away after inference
local H = input:shape()[3]
local W = input:shape()[4]
local padded_in = pad_replicate(input, OVERLAP)
local padded_out, err = run_tiled(ctx, padded_in, tile_size, OVERLAP,
job, base_pct, span_pct)
if not padded_out then return false, err end
local output = padded_out:crop(OVERLAP, OVERLAP, H, W)
output:srgb_to_linear()
local base = img.filename:match("(.+)%..+$") or img.filename
local out_path = img.path .. "/" .. base .. "_denoised.tif"
output:save_tiff(out_path, 16, img)
-- import the result and group with the source so they live together
-- in the lighttable
local imported = dt.database.import(out_path)
if imported then imported:group_with(img) end
return true
end
-- Button handler: looks up the active denoise model, fans out the
-- selected images, and reports progress via a cancellable job.
local function process_denoise()
local model_id = dt.ai.model_for_task("denoise")
if not model_id then
dt.print("no denoise model enabled -- pick one in preferences -> AI")
return
end
local images = dt.gui.selection()
if #images == 0 then
dt.print("select at least one image")
return
end
-- progress + cancel job. the cancel callback flips job.valid; the
-- main loop polls it between tiles to stop cleanly
local job = dt.gui.create_job(
string.format("AI denoise (%d image%s)",
#images, #images == 1 and "" or "s"),
true,
function(j) j.valid = false end)
dt.print_log(string.format(
"[ai_denoise] starting: %d image%s with model %s",
#images, #images == 1 and "" or "s", model_id))
local t_start = os.time()
local ctx = dt.ai.load_model(model_id)
if not ctx then
dt.print("failed to load model: " .. model_id)
job.valid = false
return
end
local span = 1.0 / #images
local done = 0
for i, img in ipairs(images) do
if not job.valid then break end
dt.print_log(string.format("[ai_denoise] [%d/%d] %s",
i, #images, img.filename))
local ok, err = denoise_one(ctx, img, TILE_SIZE, job,
(i - 1) * span, span)
if not ok then
dt.print(string.format("[%d/%d] %s: %s",
i, #images, img.filename, tostring(err)))
dt.print_log(string.format("[ai_denoise] [%d/%d] %s FAILED: %s",
i, #images, img.filename, tostring(err)))
else
done = done + 1
end
end
ctx:close()
job.valid = false
local elapsed = os.time() - t_start
dt.print(string.format("denoise complete: %d image%s",
#images, #images == 1 and "" or "s"))
dt.print_log(string.format(
"[ai_denoise] finished: %d/%d in %ds",
done, #images, elapsed))
end
-- script_manager integration: called when the user disables the script.
-- darktable can't remove a registered lib from the UI at runtime, so we
-- only stop any in-flight work; the panel itself remains until restart.
local function destroy()
-- nothing persistent to clean up: each click runs its own job and
-- closes its own model context
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.register_lib(
MODULE_NAME, -- plugin id
"AI denoise", -- displayed name
true, -- expandable
false, -- no reset button
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}},
dt.new_widget("box") {
orientation = "vertical",
dt.new_widget("button") {
label = "denoise selected",
clicked_callback = process_denoise,
}
},
nil, nil
)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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 = "AI denoise",
purpose = "tile-based AI denoise using the darktable.ai Lua API",
author = "Andrii Ryzhkov",
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/ai_denoise"
}
script_data.destroy = destroy
return script_data

View File

@@ -1,481 +0,0 @@
--[[
This file is part of darktable,
copyright (c) 2026 darktable developers.
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 <http://www.gnu.org/licenses/>.
]]
--[[
ai_raw_denoise - example script demonstrating the darktable.ai Lua
API by denoising the selected raw images with tile-based inference
on the raw CFA.
Adds an "AI raw denoise" panel to the lighttable right column with
a "denoise selected" button. Each selected image's raw CFA is
loaded, Bayer-packed to 4 channels, run tile-by-tile through the
currently enabled rawdenoise model (which denoises AND demosaicks
in one pass), and saved as a LinearRaw DNG grouped with the
source image.
Demonstrates:
* model lookup via dt.ai.model_for_task
* loading model variant files via dt.ai.load_model file arg
* raw CFA loading via dt.ai.load_raw
* tensor bayer_pack for 4-channel CFA tiling
* tile loop with non-trivial input->output scale (here 1 -> 2)
* edge-replicated padding to suppress border artifacts
* LinearRaw DNG output via dt.ai.save_dng_linear
* background job with progress bar and cancellation
LIMITATIONS
* Bayer sensors only. The script loads model_bayer.onnx and uses
bayer_pack, which assumes a 2x2 Bayer layout. X-Trans and
Foveon sensors need model_linear.onnx with a different
preprocessing pipeline (no bayer_pack, demosaicked input).
* Image must be at least TILE_SIZE x TILE_SIZE in packed pixels
(1024 x 1024 of CFA for the default 512 packed tile size).
* If a save fails mid-batch the Lua error propagates and aborts
the whole job. Wrap denoise_one in pcall if you need
batch-robust behaviour.
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
* a rawdenoise model enabled in preferences -> AI
(e.g. rawdenoise-nind)
USAGE
* require this script from your luarc
* select one or more raw images in lighttable
* click the "denoise selected" button in the AI raw denoise panel
BUGS, COMMENTS, SUGGESTIONS
* file an issue on https://github.com/darktable-org/darktable
CHANGES
]]
local dt = require "darktable"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
local MODULE_NAME = "ai_raw_denoise"
-- The rawdenoise-nind bayer model is a denoise+demosaic combo:
-- input : [1, 4, 512, 512] packed CFA (4 phase planes)
-- output : [1, 3, 1024, 1024] linear RGB at 2x the input resolution
-- So each tile-size pixel of the input covers a 2x2 CFA block, and the
-- model emits two output pixels per input pixel in each axis.
-- TILE_SIZE is in packed pixels (model input); SCALE turns those into
-- output pixels. Keep in sync with `attributes.input_sizes` in the
-- model's config.json.
local TILE_SIZE = 512 -- rawdenoise-nind
local SCALE = 2 -- model output dim / input dim
local OUT_CH = 3 -- model emits linear RGB
-- overlap between adjacent tiles, in packed pixels (input space).
-- larger values produce smoother seams at the cost of more compute
local OVERLAP = 32
-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
-- Bayer-only CFA channel lookup. Returns 0/1/2 (R/G/B) for the cell
-- at sensor position (r, c) given the 32-bit `filters` mask from
-- load_raw's meta table. Used to find the R-cell origin so bayer_pack's
-- plane 0 always holds R. X-Trans (filters == 9) uses meta.xtrans
-- instead and is not handled here.
local function fc(r, c, filters)
local shift = ((r % 2) * 4) + ((c % 2) * 2)
return math.floor(filters / (2 ^ shift)) % 4
end
-- daylight WB multipliers (R, G=1, B) derived from the camera's
-- XYZ->camRGB color matrix and the D65 white point. mirrors the C
-- path's _bayer_wb_daylight in restore_raw_bayer.c — the rawdenoise
-- bayer model is trained on daylight-WB'd input, so feeding it
-- un-WB'd camRGB produces wrong colors. Falls back to {1, 1, 1} if
-- the matrix is missing or degenerate.
local function daylight_wb(color_matrix)
local d65 = {0.95047, 1.0, 1.08883}
if not color_matrix then return 1.0, 1.0, 1.0 end
local resp = {0, 0, 0}
for c = 1, 3 do
resp[c] = color_matrix[c][1] * d65[1]
+ color_matrix[c][2] * d65[2]
+ color_matrix[c][3] * d65[3]
end
if resp[1] <= 0 or resp[2] <= 0 or resp[3] <= 0 then
return 1.0, 1.0, 1.0
end
return resp[2] / resp[1], 1.0, resp[2] / resp[3]
end
-- Surround the input with `pad` pixels on every side, replicating the
-- outermost rows/columns ("edge clamp" padding). Boundary tiles then
-- see plausible context instead of a literal image edge, so the
-- network's internal zero-padding no longer leaks into the visible
-- output as colour artifacts along the image rim.
local function pad_replicate(input, pad)
local C = input:shape()[2]
local H = input:shape()[3]
local W = input:shape()[4]
local padded = dt.ai.create_tensor({1, C, H + 2 * pad, W + 2 * pad})
-- center: original image
padded:paste(input, pad, pad)
-- left and right strips: replicate first / last column of input
local left_col = input:crop(0, 0, H, 1)
local right_col = input:crop(0, W - 1, H, 1)
for i = 0, pad - 1 do
padded:paste(left_col, pad, i)
padded:paste(right_col, pad, pad + W + i)
end
-- top and bottom strips: replicate first / last row of the already
-- horizontally-padded buffer so the four corners get filled correctly
local top_row = padded:crop(pad, 0, 1, W + 2 * pad)
local bottom_row = padded:crop(pad + H - 1, 0, 1, W + 2 * pad)
for i = 0, pad - 1 do
padded:paste(top_row, i, 0)
padded:paste(bottom_row, pad + H + i, 0)
end
return padded
end
-- Tile a [1,C_in,H,W] tensor through a model that emits [1,C_out,H*scale,
-- W*scale]. Each tile is tile_size x tile_size in input space; successive
-- tiles step by (tile_size - 2*overlap); only the central (non-overlap)
-- region of each tile's output is pasted into the result, with all
-- offsets multiplied by `scale`. Image-boundary edges keep their full
-- extent; only image-interior seams drop the overlap margin.
--
-- The trailing tile in each axis is pinned to the image boundary so
-- the residual right/bottom strip is never smaller than 2*overlap --
-- otherwise it would have to be skipped (the model has a fixed input
-- size) and those pixels would never be written.
local function run_tiled(ctx, input, tile_size, overlap, scale, c_out,
job, base_pct, span_pct)
local H = input:shape()[3]
local W = input:shape()[4]
if H < tile_size or W < tile_size then
return nil, string.format("image too small (%dx%d) for tile %d",
W, H, tile_size)
end
local step = tile_size - 2 * overlap
-- enumerate tile origins along one axis, ending exactly at the boundary
local function origins(extent)
local out, p = {}, 0
while p + tile_size < extent do
out[#out + 1] = p
p = p + step
end
out[#out + 1] = math.max(0, extent - tile_size)
return out
end
local ys, xs = origins(H), origins(W)
local total = #xs * #ys
-- output is scale-times larger spatially and may have a different
-- channel count than the input
local output = dt.ai.create_tensor({1, c_out, H * scale, W * scale})
local done = 0
for _, ty in ipairs(ys) do
for _, tx in ipairs(xs) do
if job and not job.valid then return nil, "cancelled" end
local tile_in = input:crop(ty, tx, tile_size, tile_size)
local tile_out = ctx:run(tile_in)
-- central region to keep: drop overlap on every inner edge;
-- full extent at image-boundary edges. all dimensions in OUTPUT
-- (post-scale) space, since that's where we paste
local at_top = (ty == 0)
local at_left = (tx == 0)
local at_bottom = (ty + tile_size >= H)
local at_right = (tx + tile_size >= W)
local ov = overlap * scale
local tos = tile_size * scale
local cy = at_top and 0 or ov
local cx = at_left and 0 or ov
local ch = tos - cy - (at_bottom and 0 or ov)
local cw = tos - cx - (at_right and 0 or ov)
output:paste(tile_out:crop(cy, cx, ch, cw),
ty * scale + cy, tx * scale + cx)
done = done + 1
if job then
job.percent = base_pct + span_pct * (done / total)
end
end
end
return output
end
-- Denoise one raw image. The bayer rawdenoise model denoises AND
-- demosaicks in one pass, so its output is linear RGB at the
-- original CFA resolution (not a CFA mosaic). Saved as a LinearRaw
-- DNG, which darktable can re-import as an already-debayered raw.
--
-- Pipeline:
-- load CFA [1,1,H,W] + image metadata
-- -> bayer_pack [1,4,H/2,W/2]
-- -> normalize to [0, 1] using black/white levels (the model is
-- trained on (val - black) / (white - black), not raw ADC)
-- -> pad to [1,4,H/2+2P,W/2+2P]
-- -> tile through model (scale=2, c_out=3) -> [1,3,(H+4P),(W+4P)]
-- -> crop padding back off -> [1,3,H,W]
-- -> save linear DNG -> import + group with source
local function denoise_one(ctx, img, tile_size, job, base_pct, span_pct)
-- load_raw returns (tensor, meta). tensor is [1,1,H,W] in raw ADC
-- units; meta carries black/white/wb/CFA info we need to normalise
-- the input into the [0, 1] range the model was trained on
local cfa, meta = dt.ai.load_raw(img)
if not cfa then return false, "load_raw failed" end
-- this script is Bayer-only: filters == 9 is X-Trans (handled by a
-- different preprocessing pipeline + model_linear.onnx); filters == 0
-- means monochrome / non-CFA, which bayer_pack can't handle either
if not meta.filters or meta.filters == 0 or meta.filters == 9 then
return false, "unsupported CFA pattern (Bayer-only script)"
end
-- Crop to the visible region and shift the origin onto the R cell
-- of the CFA pattern. bayer_pack splits by position only, so to make
-- plane 0 always hold R (which the model expects) the cropped CFA's
-- (0, 0) must sit at an R site. Mirrors the C path's _bayer_origin
-- + RGGB shift in restore_raw_bayer.c.
local cy = 0
local cx = 0
local cw = cfa:shape()[4]
local ch = cfa:shape()[3]
if meta.crop_x and meta.crop_y then
cy = meta.crop_y - (meta.crop_y % 2)
cx = meta.crop_x - (meta.crop_x % 2)
cw = (meta.visible_width or cw) + (meta.crop_x - cx)
ch = (meta.visible_height or ch) + (meta.crop_y - cy)
end
-- find R cell within the 2x2 starting at (cy, cx)
local sy, sx = 0, 0
for ty = 0, 1 do
for tx = 0, 1 do
if fc(cy + ty, cx + tx, meta.filters) == 0 then
sy, sx = ty, tx
end
end
end
cy = cy + sy
cx = cx + sx
ch = ch - sy
cw = cw - sx
-- ensure even dims for bayer_pack
ch = ch - (ch % 2)
cw = cw - (cw % 2)
if cy > 0 or cx > 0
or cw < cfa:shape()[4] or ch < cfa:shape()[3]
then
cfa = cfa:crop(cy, cx, ch, cw)
end
-- uniform-black approximation: most sensors report identical black
-- levels across the 4 CFA sites, in which case a single scalar
-- (val - black) / (white - black) normalisation is exact. cameras
-- with non-uniform per-site black levels (e.g. some PDAF sensors)
-- would need per-plane scaling via scale_add_planes -- left as an
-- exercise
local black = meta.black_level
local range = meta.white_level - black
if range <= 0 then range = 65535 end
-- pack the 1-channel CFA into 4 phase planes so each channel holds
-- one Bayer site type. After the RGGB shift above:
-- plane 0 = R, planes 1,2 = G, plane 3 = B
local packed = cfa:bayer_pack()
packed:scale_add(1.0 / range, -black / range)
-- apply daylight WB so the four planes sit in the same range the
-- model was trained on (R, G, G, B → R*wb_R, G, G, B*wb_B). without
-- this, the model sees unbalanced channels and produces wrong colors
local wb_R, wb_G, wb_B = daylight_wb(meta.color_matrix)
packed:scale_add_planes({wb_R, wb_G, wb_G, wb_B})
local pH = packed:shape()[3]
local pW = packed:shape()[4]
-- save the post-normalisation input mean for gain-matching the
-- model output: the bayer rawdenoise model emits values on its own
-- internal scale, not [0, 1], so we rescale so output mean matches
-- input mean (the per-tile equivalent of dt's C reference path)
local in_mean = packed:mean()
-- pad so the image rim is no longer at a tile boundary; the padded
-- strip absorbs the network's edge artifacts and is cropped away
-- after inference (in output space, where the pad covers 2*OVERLAP
-- pixels per side)
local padded_in = pad_replicate(packed, OVERLAP)
local padded_out, err = run_tiled(ctx, padded_in, tile_size, OVERLAP,
SCALE, OUT_CH,
job, base_pct, span_pct)
if not padded_out then return false, err end
-- crop the padded region (in output coords: pad * scale per side)
-- back to the original-CFA resolution
local out_rgb = padded_out:crop(OVERLAP * SCALE, OVERLAP * SCALE,
pH * SCALE, pW * SCALE)
-- gain-match: rescale the output so its mean matches the input
-- mean, bringing the model's internal-scale output back into the
-- [0, 1] range save_dng_linear expects
local out_mean = out_rgb:mean()
if out_mean > 1e-8 then
out_rgb:scale_add(in_mean / out_mean)
end
-- invert the daylight WB so the saved tensor is un-WB'd camRGB
-- (save_dng_linear's contract — the consumer applies AsShotNeutral
-- on import)
out_rgb:scale_add_planes({1.0 / wb_R, 1.0 / wb_G, 1.0 / wb_B})
local base = img.filename:match("(.+)%..+$") or img.filename
local out_path = img.path .. "/" .. base .. "_rawdenoised.dng"
-- LinearRaw DNG: tensor values must be normalised to [0, 1] camRGB;
-- the model handles black-subtract + scale internally, so we just
-- pass the output through
dt.ai.save_dng_linear(out_rgb, img, out_path)
-- import the result and group with the source so they live together
-- in the lighttable
local imported = dt.database.import(out_path)
if imported then imported:group_with(img) end
return true
end
-- Button handler: looks up the active rawdenoise model, fans out the
-- selected images, and reports progress via a cancellable job.
local function process_raw_denoise()
local model_id = dt.ai.model_for_task("rawdenoise")
if not model_id then
dt.print("no rawdenoise model enabled -- pick one in preferences -> AI")
return
end
local images = dt.gui.selection()
if #images == 0 then
dt.print("select at least one image")
return
end
-- progress + cancel job. the cancel callback flips job.valid; the
-- main loop polls it between tiles to stop cleanly
local job = dt.gui.create_job(
string.format("AI raw denoise (%d image%s)",
#images, #images == 1 and "" or "s"),
true,
function(j) j.valid = false end)
dt.print_log(string.format(
"[ai_raw_denoise] starting: %d image%s with model %s",
#images, #images == 1 and "" or "s", model_id))
local t_start = os.time()
-- rawdenoise packages ship multiple files (one per CFA type); pick
-- the Bayer variant. X-Trans / Foveon would use model_linear.onnx
-- with a different preprocessing pipeline (no bayer_pack)
local ctx = dt.ai.load_model(model_id, nil, "model_bayer.onnx")
if not ctx then
dt.print("failed to load model: " .. model_id)
job.valid = false
return
end
local span = 1.0 / #images
local done = 0
for i, img in ipairs(images) do
if not job.valid then break end
dt.print_log(string.format("[ai_raw_denoise] [%d/%d] %s",
i, #images, img.filename))
local ok, err = denoise_one(ctx, img, TILE_SIZE, job,
(i - 1) * span, span)
if not ok then
dt.print(string.format("[%d/%d] %s: %s",
i, #images, img.filename, tostring(err)))
dt.print_log(string.format("[ai_raw_denoise] [%d/%d] %s FAILED: %s",
i, #images, img.filename, tostring(err)))
else
done = done + 1
end
end
ctx:close()
job.valid = false
local elapsed = os.time() - t_start
dt.print(string.format("raw denoise complete: %d image%s",
#images, #images == 1 and "" or "s"))
dt.print_log(string.format(
"[ai_raw_denoise] finished: %d/%d in %ds",
done, #images, elapsed))
end
-- script_manager integration: called when the user disables the script.
-- darktable can't remove a registered lib from the UI at runtime, so we
-- only stop any in-flight work; the panel itself remains until restart.
local function destroy()
-- nothing persistent to clean up: each click runs its own job and
-- closes its own model context
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.register_lib(
MODULE_NAME, -- plugin id
"AI raw denoise", -- displayed name
true, -- expandable
false, -- no reset button
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}},
dt.new_widget("box") {
orientation = "vertical",
dt.new_widget("button") {
label = "denoise selected",
clicked_callback = process_raw_denoise,
}
},
nil, nil
)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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 = "AI raw denoise",
purpose = "tile-based AI raw denoise using the darktable.ai Lua API",
author = "Andrii Ryzhkov",
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/ai_raw_denoise"
}
script_data.destroy = destroy
return script_data

View File

@@ -23,14 +23,6 @@ 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()
@@ -38,21 +30,12 @@ local function destroy()
end
local result = dt.configuration.api_version_string
dt.print_log("API Version: " .. result)
dt.print("API " .. _("version") .. ": " .. result)
dt.print_error("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

View File

@@ -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_demo") -- darktable 3.0
du.check_min_api_version("5.0.2", "darkroom_mode") -- darktable 3.0
-- script_manager integration to allow a script to be removed
-- without restarting darktable
@@ -52,16 +52,17 @@ end
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
local MODULE_NAME = "darkroom_demo"
local MODULE_NAME = "darkroom"
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
local gettext = dt.gettext
gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS)
local function _(msgid)
return gettext(msgid)
return gettext.dgettext(MODULE_NAME, msgid)
end
-- - - - - - - - - - - - - - - - - - - - - - - -
@@ -91,13 +92,13 @@ dt.gui.current_view(dt.gui.views.darkroom)
local max_images = 10
dt.print(_("showing images, with a pause between each"))
dt.print(_("Showing images, with a pause in 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(string.format(_("displaying image "), i))
dt.print(_("Displaying image " .. i))
dt.gui.views.darkroom.display_image(img)
sleep(1500)
if i == max_images then
@@ -107,7 +108,7 @@ end
-- return to lighttable view
dt.print(_("restoring view"))
dt.print(_("Restoring view"))
sleep(1500)
dt.gui.current_view(current_view)
@@ -115,14 +116,6 @@ 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 <wpferguson@gmail.com>",
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/darkroom_demo"
}
script_data.destroy = destroy
return script_data

View File

@@ -67,31 +67,27 @@ end
-- Not translated Text
dt.print_error("Hello World!")
local gettext = dt.gettext.gettext
local gettext = dt.gettext
-- Translate a string using the darktable textdomain
dt.print_error(gettext("image"))
dt.print_error(gettext.gettext("image"))
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("gettextExample",dt.configuration.config_dir.."/lua/locale/")
-- Translate a string using the specified textdomain
dt.print_error(gettext.dgettext("gettextExample", 'Hello World!'))
-- 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(msgid)
return gettext.dgettext("gettextExample", 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

View File

@@ -4,61 +4,39 @@ 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'")
placeholder = "action path",
tooltip = "enter the full path of an action, for example 'lib/filter/view'"
}
wg.instance = dt.new_widget("combobox"){
label = _("instance"),
tooltip = _("the instance of an image processing module to execute action on"),
label = "instance",
tooltip = "the instance of an image processing module to execute action on",
"0", "+1", "-1", "+2", "-2", "+3", "-3", "+4", "-4", "+5", "-5", "+6", "-6", "+7", "-7", "+8", "-8", "+9", "-9"
}
wg.element = dt.new_widget("entry"){
text = "",
placeholder = _("action element"),
tooltip = _("enter the element of an action, for example 'selection', or leave empty for default")
placeholder = "action element",
tooltip = "enter the element of an action, for example 'selection', or leave empty for default"
}
wg.effect = dt.new_widget("entry"){
text = "next",
placeholder = _("action effect"),
tooltip = _("enter the effect of an action, for example 'next', or leave empty for default")
placeholder = "action effect",
tooltip = "enter the effect of an action, for example 'next', or leave empty for default"
}
wg.speed = dt.new_widget("entry"){
text = "1",
placeholder = _("action speed"),
tooltip = _("enter the speed to use in action execution, or leave empty to only read state")
placeholder = "action speed",
tooltip = "enter the speed to use in action execution, or leave empty to only read state"
}
wg.check = dt.new_widget("check_button"){
label = _('perform action'),
tooltip = _('perform action or only read return'),
label = 'perform action',
tooltip = 'perform action or only read return',
clicked_callback = function()
wg.speed.sensitive = wg.check.value
end,
@@ -72,7 +50,7 @@ wg.return_value = dt.new_widget("entry"){
dt.register_lib(
"execute_action", -- Module name
_("execute gui actions"), -- name
"execute gui actions", -- name
true, -- expandable
false, -- resetable
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 100},
@@ -84,37 +62,37 @@ dt.register_lib(
dt.new_widget("box")
{
orientation = "horizontal",
dt.new_widget("label"){label = _("action path"), halign = "start"},
dt.new_widget("label"){label = "action path", halign = "start"},
wg.action
},
wg.instance,
dt.new_widget("box")
{
orientation = "horizontal",
dt.new_widget("label"){label = _("element"), halign = "start"},
dt.new_widget("label"){label = "element", halign = "start"},
wg.element
},
dt.new_widget("box")
{
orientation = "horizontal",
dt.new_widget("label"){label = _("effect"), halign = "start"},
dt.new_widget("label"){label = "effect", halign = "start"},
wg.effect
},
wg.check,
dt.new_widget("box")
{
orientation = "horizontal",
dt.new_widget("label"){label = _("speed"), halign = "start"},
dt.new_widget("label"){label = "speed", halign = "start"},
wg.speed
},
dt.new_widget("button")
{
label = _("execute action"),
tooltip = _("execute the action specified in the fields above"),
label = "execute action",
tooltip = "execute the action specified in the fields above",
clicked_callback = function(_)
local sp = NaN
if wg.check.value then sp = wg.speed.text end
wg.return_value.text = dt.gui.action(wg.action.text, tonumber(wg.instance.value), wg.element.text, wg.effect.text, tonumber(sp))
wg.return_value.text = dt.gui.action(wg.action.text, wg.instance.value, wg.element.text, wg.effect.text, sp)
end
},
dt.new_widget("box")
@@ -125,18 +103,3 @@ dt.register_lib(
},
}
)
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

View File

@@ -29,36 +29,20 @@ 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

View File

@@ -59,12 +59,11 @@ end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- T R A N S L A T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
local gettext = dt.gettext.gettext
dt.gettext.bindtextdomain("lighttable_demo", dt.configuration.config_dir .."/lua/locale/")
local gettext = dt.gettext
gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS)
local function _(msgid)
return gettext(msgid)
return gettext.dgettext(MODULE_NAME, msgid)
end
-- - - - - - - - - - - - - - - - - - - - - - - -
@@ -146,17 +145,17 @@ sleep(2000)
for n, layout in ipairs(layouts) do
dt.gui.libs.lighttable_mode.layout(layout)
dt.print(string.format(_("set lighttable layout to %s"), layout))
dt.print_log("set lighttable layout to " .. layout)
dt.print(_("set lighttable layout to " .. 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(string.format(_("set zoom level to %d"), i))
dt.print(_("Set zoom level to " .. i))
sleep(1500)
end
for i = 9, 1, -1 do
dt.gui.libs.lighttable_mode.zoom_level(i)
dt.print(string.format(_("set zoom level to %d"), i))
dt.print(_("Set zoom level to " .. i))
sleep(1500)
end
end
@@ -175,12 +174,12 @@ dt.print_log("starting sorts")
for n, sort in ipairs(sorts) do
dt.gui.libs.filter.sort(sort)
dt.print(string.format(_("set lighttable sort to %s"), sort))
dt.print(_("set lighttable sort to " .. sort))
sleep(1500)
for m, sort_order in ipairs(sort_orders) do
dt.gui.libs.filter.sort_order(sort_order)
dt.print(string.format(_("sort order set to %s"), sort_order))
dt.print(_("sort order set to " .. sort_order))
sleep(1500)
end
end
@@ -191,12 +190,12 @@ dt.print(_("lighttable filtering demonstration"))
for n, rating in ipairs(ratings) do
dt.gui.libs.filter.rating(rating)
dt.print(string.format(_("set filter to %s"), rating))
dt.print(_("set filter to " .. rating))
sleep(1500)
for m, rating_comparator in ipairs(rating_comparators) do
dt.gui.libs.filter.rating_comparator(rating_comparator)
dt.print(string.format(_("set rating comparator to %s"), rating_comparator))
dt.print(_("set rating comparator to " .. rating_comparator))
sleep(1500)
end
end
@@ -216,14 +215,6 @@ 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 <wpferguson@gmail.com>",
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/lighttable_demo"
}
script_data.destroy = destroy
return script_data

View File

@@ -35,24 +35,10 @@ 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
@@ -60,6 +46,15 @@ script_data.show = nil -- only required for libs since the destroy_method only h
-- 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 = {}
@@ -75,7 +70,7 @@ local function install_module()
-- https://www.darktable.org/lua-api/index.html#darktable_register_lib
dt.register_lib(
"exampleModule", -- Module name
_("example module"), -- name
"exampleModule", -- name
true, -- expandable
false, -- resetable
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers
@@ -85,9 +80,9 @@ local function install_module()
orientation = "vertical",
dt.new_widget("button")
{
label = _("my ") .. "button",
label = _("MyButton"),
clicked_callback = function (_)
dt.print(_("button clicked"))
dt.print(_("Button clicked"))
end
},
table.unpack(mE.widgets),
@@ -110,10 +105,10 @@ local function restart()
end
-- https://www.darktable.org/lua-api/types_lua_check_button.html
local check_button = dt.new_widget("check_button"){label = _("my ") .. "check_button", value = true}
local check_button = dt.new_widget("check_button"){label = _("MyCheck_button"), value = true}
-- https://www.darktable.org/lua-api/types_lua_combobox.html
local combobox = dt.new_widget("combobox"){label = _("my ") .. "combobox", value = 2, "8", "16", "32"}
local combobox = dt.new_widget("combobox"){label = _("MyCombobox"), value = 2, "8", "16", "32"}
-- https://www.darktable.org/lua-api/types_lua_entry.html
local entry = dt.new_widget("entry")
@@ -122,21 +117,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 = _("my ") .. "file_chooser_button", -- The title of the window when choosing a file
title = _("MyFile_chooser_button"), -- The title of the window when choosing a file
value = "", -- The currently selected file
is_directory = false -- True if the file chooser button only allows directories to be selecte
}
-- https://www.darktable.org/lua-api/types_lua_label.html
local label = dt.new_widget("label")
label.label = _("my ") .. "label" -- This is an alternative way to the "{}" syntax to set a property
label.label = _("MyLabel") -- 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"){}
@@ -144,7 +139,7 @@ local separator = dt.new_widget("separator"){}
-- https://www.darktable.org/lua-api/types_lua_slider.html
local slider = dt.new_widget("slider")
{
label = _("my ") .. "slider",
label = _("MySlider"),
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

View File

@@ -29,7 +29,8 @@
by hovering over the image and pressing the key combination.
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
* exiftool - https://exiftool.org
* ufraw-batch - https://ufraw.sourceforge.net
MacOS - install with homebrew
USAGE
* require this script from your main lua file
@@ -43,7 +44,6 @@
* Send to Bill Ferguson, wpferguson@gmail.com
CHANGES
20251205 - replaced ufraw-batch with exiftool
]]
--[[
@@ -70,10 +70,13 @@ 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.gettext
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 function _(msgid)
return gettext(msgid)
return gettext.dgettext("multi_os", msgid)
end
--[[
@@ -139,36 +142,6 @@ local function copy_image_attributes(from, to, ...)
end
end
local function check_file_size(img_name)
local result = 0
if dt.configuration.running_os == "windows" then
local path = df.get_path(img_name)
local fname = df.get_filename(img_name)
local p = io.open("forfiles /P " .. path .. " /M " .. fname .. " /C \"cmd /c echo $fsize\"")
if p then
for line in p:lines() do
if line:length() > 5 then
result = tonumber(line)
end
end
p:close()
end
else
local p = io.popen("ls -s " .. img_name)
if p then
local line = p:read("*a")
result = tonumber(du.split(line, " ")[1])
p:close()
end
end
return result
end
--[[
The main function called from the button or the shortcut. It takes one or more raw files, passed
in a table and extracts the embedded jpeg images
@@ -182,12 +155,11 @@ local function extract_embedded_jpeg(images)
in the user's path. When it finds the executable, it returns the command to run it.
]]
local exiftool_executable = df.check_if_bin_exists("exiftool")
if exiftool_executable then
local ufraw_executable = df.check_if_bin_exists("ufraw-batch")
if ufraw_executable then
for _, image in ipairs(images) do
if image.is_raw then
local img_file = tostring(image)
local jpg_img_file = string.gsub(img_file, "%....$", ".jpg")
local img_file = du.join({image.path, image.filename}, "/")
--[[
dtsys.external_command() is operating system aware and formats the command as necessary.
@@ -195,12 +167,8 @@ local function extract_embedded_jpeg(images)
operating system aware and uses the corresponding quotes.
]]
if dtsys.external_command(exiftool_executable .. " -b -JpgFromRaw " .. df.sanitize_filename(img_file) .. " > " .. df.sanitize_filename(jpg_img_file)) then
dt.print_log("created jpg from raw")
if check_file_size(df.sanitize_filename(jpg_img_file)) == 0 then
dt.print_log("using preview")
dtsys.external_command(exiftool_executable .. " -b -PreviewImage " .. df.sanitize_filename(img_file) .. " > " .. df.sanitize_filename(jpg_img_file))
end
if dtsys.external_command(ufraw_executable .. " --silent --embedded-image " .. df.sanitize_filename(img_file)) then
local jpg_img_file = df.chop_filetype(img_file) .. ".embedded.jpg"
dt.print_log("jpg_img_file set to ", jpg_img_file) -- print a debugging message
local myimage = dt.database.import(jpg_img_file)
myimage:group_with(image.group_leader)
@@ -219,12 +187,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(string.format(_("%s is not a raw file, no image can be extracted"), image.filename)) -- print the error to the screen
dt.print(image.filename .. " is not a raw file. No image can be extracted") -- print the error to the screen
end
end
else
dt.print_error("exiftool not found. Exiting...") -- print debugging error message
dt.print("exiftool not found, exiting...") -- print the error to the screen
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
end
end
@@ -248,7 +216,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 = string.format(_("select %s executable"), "ufraw-batch[.exe]"),
title = _("Select ufraw-batch[.exe] executable"),
value = df.get_executable_path_preference(executable),
is_directory = false,
changed_callback = function(self)
@@ -259,8 +227,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
)
@@ -273,7 +241,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"
)
--[[
@@ -283,7 +251,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"
)
--[[
@@ -293,14 +261,6 @@ dt.register_event(
]]
local script_data = {}
script_data.metadata = {
name = _("multi OS"),
purpose = _("example module thet runs on different operating systems"),
author = "Bill Ferguson <wpferguson@gmail.com>",
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/multi_os"
}
script_data.destroy = destroy
return script_data

View File

@@ -53,16 +53,17 @@ end
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
local MODULE_NAME = "panels_demo"
local MODULE_NAME = "panels"
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
local gettext = dt.gettext
gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS)
local function _(msgid)
return gettext(msgid)
return gettext.dgettext(MODULE_NAME, msgid)
end
-- - - - - - - - - - - - - - - - - - - - - - - -
@@ -93,29 +94,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 time"))
dt.print(_("Hiding all panels, one at a tme"))
sleep(1500)
for i = 1, #panels do
dt.print(string.format(_("hiding %s"), panels[i]))
dt.print(_("Hiding " .. 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(string.format(_("showing %s"), panels[i]))
dt.print(_("Showing " .. 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()
@@ -123,7 +124,7 @@ sleep(1500)
-- show all
dt.print(_("showing all panels"))
dt.print(_("Showing all panels"))
sleep(1500)
dt.gui.panel_show_all()
@@ -131,7 +132,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])
@@ -144,14 +145,6 @@ 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 <wpferguson@gmail.com>",
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/panels_demo"
}
script_data.destroy = destroy
return script_data

View File

@@ -24,45 +24,28 @@ 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
@@ -70,8 +53,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
@@ -80,30 +63,22 @@ dt.preferences.register("preferenceExamples", -- script: This is a string
dt.preferences.register("preferenceExamples", -- script: This is a string used to avoid name collision in preferences (i.e namespace). Set it to something unique, usually the name of the script handling the preference.
"preferenceExamplesFile", -- name
"file", -- type
_("example") .. " file", -- label
_("example") .. " file " .. _("tooltip"), -- tooltip
"Example File", -- label
"Example File Tooltip", -- tooltip
"") -- default
dt.preferences.register("preferenceExamples", -- script: This is a string used to avoid name collision in preferences (i.e namespace). Set it to something unique, usually the name of the script handling the preference.
"preferenceExamplesDirectory", -- name
"directory", -- type
_("example") .. " directory", -- label
_("example") .. " directory " .. _("tooltip"), -- tooltip
"Example Directory", -- label
"Example Directory Tooltip", -- tooltip
"") -- default
dt.preferences.register("preferenceExamples", -- script: This is a string used to avoid name collision in preferences (i.e namespace). Set it to something unique, usually the name of the script handling the preference.
"preferenceExamplesEnum", -- name
"enum", -- type
_("example") .. " enum", -- label
_("example") .. " enum " .. _("tooltip"), -- tooltip
"Example Enum", -- label
"Example Enum Tooltip", -- tooltip
"Enum 1", -- default
"Enum 1", "Enum 2") -- values
local function destroy()
-- nothing to destroy
end
script_data.destroy = destroy
return script_data

View File

@@ -24,14 +24,6 @@ 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()
@@ -40,7 +32,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
@@ -56,14 +48,6 @@ 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

View File

@@ -30,34 +30,18 @@ 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(string.format(_("you are running: %s"), dt.configuration.running_os))
dt.print("You are running: "..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

View File

@@ -1,345 +1,163 @@
--[[
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 <http://www.gnu.org/licenses/>.
]]
--[[
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
USAGE:
To add a new module, simply add an entry to MODULE_PROFILES below.
Get action paths from darktable's shortcut interface.
]]
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
-- Constants
local NUM_KNOBS = 8
local NAN = 0/0
local darkroom_view = dt.gui.views.darkroom
-- ============================================================================
-- USER CONFIGURATION
-- ============================================================================
-- Module Profiles: Define behavior when each module has focus
-- Structure: [module_name] = { knob1_action, knob2_action, ... }
local MODULE_PROFILES = {
-- Color Zones: Control individual color channels
colorzones = {
{"iop/colorzones/graph", "red"},
{"iop/colorzones/graph", "orange"},
{"iop/colorzones/graph", "yellow"},
{"iop/colorzones/graph", "green"},
{"iop/colorzones/graph", "aqua"},
{"iop/colorzones/graph", "blue"},
{"iop/colorzones/graph", "purple"},
{"iop/colorzones/graph", "magenta"}
},
-- Color Equalizer: Control color channels
colorequal = {
{"iop/colorequal/graph", "red"},
{"iop/colorequal/graph", "orange"},
{"iop/colorequal/graph", "yellow"},
{"iop/colorequal/graph", "green"},
{"iop/colorequal/graph", "cyan"},
{"iop/colorequal/graph", "blue"},
{"iop/colorequal/graph", "lavender"},
{"iop/colorequal/graph", "magenta"}
},
-- Sigmoid: Control primary color adjustments
sigmoid = {
{"iop/sigmoid/primaries/red attenuation"},
{"iop/sigmoid/primaries/red rotation"},
{"iop/sigmoid/primaries/green attenuation"},
{"iop/sigmoid/primaries/green rotation"},
{"iop/sigmoid/primaries/blue attenuation"},
{"iop/sigmoid/primaries/blue rotation"},
{"iop/sigmoid/primaries/recover purity"}
},
-- Color Calibration (Primaries)
primaries = {
{"iop/primaries/red hue"},
{"iop/primaries/red purity"},
{"iop/primaries/green hue"},
{"iop/primaries/green purity"},
{"iop/primaries/blue hue"},
{"iop/primaries/blue purity"},
{"iop/primaries/tint hue"},
{"iop/primaries/tint purity"}
},
-- AgX
agx = {
{"iop/agx/white relative exposure"},
{"iop/agx/black relative exposure"},
{"iop/agx/curve/pivot relative exposure"},
{"iop/agx/curve/pivot target output"},
{"iop/agx/curve/contrast"},
{"iop/agx/curve/shoulder power"},
{"iop/agx/curve/toe power"},
{"iop/agx/curve/curve y gamma"}
},
-- Tone Equalizer: Control EV bands
-- For 8 knobs, using offsets -8 to get bands
toneequal = {
{"iop/toneequal/simple/-8 EV"},
{"iop/toneequal/simple/-7 EV"},
{"iop/toneequal/simple/-6 EV"},
{"iop/toneequal/simple/-5 EV"},
{"iop/toneequal/simple/-4 EV"},
{"iop/toneequal/simple/-3 EV"},
{"iop/toneequal/simple/-2 EV"},
{"iop/toneequal/simple/-1 EV"}
},
-- Color Balance RGB: Selected controls
colorbalancergb = {
nil, -- Knob 1: unused --> default
{"iop/colorbalancergb/global vibrance"},
{"iop/colorbalancergb/contrast"},
{"iop/colorbalancergb/global chroma"},
{"iop/colorbalancergb/global saturation"},
{"iop/colorbalancergb/global brilliance"},
nil, -- Knob 7: --> default
nil -- Knob 8: --> default
},
-- Channel Mixer RGB (Color Calibration): Tab control and sliders
channelmixerrgb = {
nil, -- Knob 1-4: --> default
nil,
nil,
nil,
{"iop/channelmixerrgb/page", "CAT", "tab"}, -- Knob 5: Tab selector
{"iop/focus/sliders", "1st"}, -- Knob 6: First slider
{"iop/focus/sliders", "2nd"}, -- Knob 7: Second slider
{"iop/focus/sliders", "3rd"} -- Knob 8: Third slider
},
}
local DEFAULT_MAPPINGS = {
{"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"}
}
-- Mask properties: Always checked first, regardless of module focus
local MASK_PROPERTIES = {
{"lib/masks/properties/opacity"},
{"lib/masks/properties/size"},
{"lib/masks/properties/feather"},
{"lib/masks/properties/hardness"},
{"lib/masks/properties/rotation"},
{"lib/masks/properties/curvature"},
{"lib/masks/properties/compression"}
}
-- Priority order for checking module focus (most commonly used first)
local MODULE_CHECK_ORDER = {
"toneequal",
"colorequal",
"colorbalancergb",
"agx",
"sigmoid",
"primaries",
"channelmixerrgb",
"colorzones"
}
-- ============================================================================
-- CORE LOGIC - No need to edit below unless changing architecture
-- ============================================================================
-- Helper functions
local function is_in_darkroom()
return dt.gui.current_view() == darkroom_view
end
local function is_valid(value)
return value == value -- NaN check
end
local function translate_tab_effect(effect)
if effect == "up" then
return "next"
elseif effect == "down" then
return "previous"
else
return "activate"
end
end
-- Execute an action configuration
local function execute_action(action_config, element, effect, size)
if not action_config then
return nil
end
local path = action_config[1]
local elem = action_config[2] or element
local action_type = action_config[3]
-- Handle special action types
if action_type == "tab" then
effect = translate_tab_effect(effect)
end
return dt.gui.action(path, elem, effect, size)
end
-- Try mask properties first
local function try_mask_properties(knob_idx, element, effect, size)
if knob_idx >= NUM_KNOBS then
return NAN
end
local mask_action = MASK_PROPERTIES[knob_idx]
if not mask_action then
return NAN
end
local value = execute_action(mask_action, element, effect, size)
return value
end
-- Main knob handler
local function handle_knob_action(knob_idx, element, effect, size)
-- Early exit for non-darkroom view
if not is_in_darkroom() then
return NAN
end
-- Try mask properties first (no focus check needed)
local mask_value = try_mask_properties(knob_idx, element, effect, size)
if is_valid(mask_value) then
return mask_value
end
-- Check focused modules in priority order
for _, module_name in ipairs(MODULE_CHECK_ORDER) do
local focus_status = dt.gui.action("iop/" .. module_name, "focus")
if focus_status ~= 0 then
-- Module has focus, get its profile
local profile = MODULE_PROFILES[module_name]
if profile then
local action_config = profile[knob_idx]
-- If profile has explicit mapping for this knob, use it
if action_config then
local result = execute_action(action_config, element, effect, size)
if result then
return result
end
else
-- Profile exists but this knob is nil -> use default
-- This avoids checking other modules when we know the focused one
local default_action = DEFAULT_MAPPINGS[knob_idx]
return execute_action(default_action, element, effect, size) or NAN
end
end
-- If no profile exists for this module, continue checking others
end
end
-- No module has focus or no profile matched, fall back to default
local default_action = DEFAULT_MAPPINGS[knob_idx]
return execute_action(default_action, element, effect, size) or NAN
end
-- Setup mimic sliders
local function setup_knobs()
for k = 1, NUM_KNOBS do
dt.gui.mimic("slider", "knob " .. k,
function(action, element, effect, size)
local knob_idx = tonumber(action:sub(-1))
return handle_knob_action(knob_idx, element, effect, size)
end)
end
end
-- Script metadata
local script_data = {
metadata = {
name = _("x-touch"),
purpose = _("data-driven x-touch midi controller configuration"),
author = "Martin Straeten",
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/x-touch"
},
destroy = function() end,
destroy_method = nil,
restart = nil,
show = nil
}
-- Initialize
setup_knobs()
return script_data
--[[
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 <http://www.gnu.org/licenses/>.
]]
--[[
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
]]
local dt = require "darktable"
local du = require "lib/dtutils"
du.check_min_api_version("9.2.0", "x-touch")
-- 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]
-- 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

View File

@@ -61,7 +61,7 @@ dtutils.libdoc.functions["check_min_api_version"] = {
function dtutils.check_min_api_version(min_api, script_name)
local current_api = dt.configuration.api_version_string
if dtutils.compare_api_versions(min_api, current_api) > 0 then
if min_api > current_api then
dt.print_error("This application is written for lua api version " .. min_api .. " or later.")
dt.print_error("The current lua api version is " .. current_api)
dt.print("ERROR: " .. script_name .. " failed to load. Lua API version " .. min_api .. " or later required.")
@@ -82,7 +82,8 @@ dtutils.libdoc.functions["check_max_api_version"] = {
run against the current api version. This function is used when a part of the Lua API that
the script relies on is removed. If the maximum api version is not met, then an
error message is printed saying the script_name failed to load, then an error is thrown causing the
program to stop executing.]],
program to stop executing.
Return_Value = [[result - true if the maximum api version is available, false if not.]],
Limitations = [[When using the default handler on a script being executed from the luarc file, the error thrown
will stop the luarc file from executing any remaining statements. This limitation does not apply to script_manger.]],
@@ -96,7 +97,7 @@ dtutils.libdoc.functions["check_max_api_version"] = {
function dtutils.check_max_api_version(max_api, script_name)
local current_api = dt.configuration.api_version_string
if dtutils.compare_api_versions(current_api, max_api) > 0 then
if current_api > max_api 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.")
@@ -374,7 +375,7 @@ dtutils.libdoc.functions["deprecated"] = {
du.deprecated(script_name, removal_string)
script_name - name of the script being deprecated
removal_string - a string explaining when the script will be removed]],
removal_strubg - 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 = [[]],
@@ -391,77 +392,5 @@ 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

View File

@@ -24,12 +24,15 @@ dtutils_file.libdoc = {
functions = {}
}
local gettext = dt.gettext.gettext
local gettext = dt.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(msgid)
return gettext.dgettext("dtutils.file", msgid)
end
--[[
@@ -519,9 +522,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 ' .. dtutils_file.sanitize_filename(fromFile) .. ' ' .. dtutils_file.sanitize_filename(toFile))
result = os.execute('copy "' .. fromFile .. '" "' .. toFile .. '"')
elseif dtutils_file.check_if_bin_exists("cp") then
result = os.execute("cp " .. dtutils_file.sanitize_filename(fromFile) .. ' ' .. dtutils_file.sanitize_filename(toFile))
result = os.execute("cp '" .. fromFile .. "' '" .. toFile .. "'")
end
-- if cp was not present, or if cp failed, then a pure lua solution
@@ -572,7 +575,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 " .. dtutils_file.sanitize_filename(fromFile) .. ' ' .. dtutils_file.sanitize_filename(toFile))
success = os.execute("mv '" .. fromFile .. "' '" .. toFile .. "'")
end
-- if the mv didn't exist or succeed, then...
if not success then
@@ -812,7 +815,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.." "..dtutils_file.sanitize_filename(path))
return dsys.external_command(mkdir_cmd.." "..path)
else
return 0
end
@@ -837,7 +840,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.." "..dtutils_file.sanitize_filename(path))
return dsys.external_command(rm_cmd.." "..path)
end
dtutils_file.libdoc.functions["create_tmp_file"] = {
@@ -858,6 +861,9 @@ 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

View File

@@ -199,7 +199,7 @@ function dtutils_log.msg(level, ...)
table.remove(args, 1)
end
local log_msg = level.label
if level.engine ~= dt_print and call_level ~= 0 then
if level.engine ~= dt_screen 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 .. " "

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
local dtutils_system = {}
local dt = require "darktable"
local ds = require "lib/dtutils.string"
dtutils_system.libdoc = {
Name = [[dtutils.system]],
@@ -86,15 +85,8 @@ function dtutils_system.windows_command(command)
local file = io.open(fname, "w")
if file then
dt.print_log("opened file")
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("set COMMAND_RC=%ERRORLEVEL%")
file:write("\n")
file:write("chcp %cp%>nul\n")
file:write("exit /b %COMMAND_RC%") -- otherwise, when the real command fails, script will still return "good" error code of chcp
command = string.gsub(command, "%%", "%%%%") -- escape % from windows shell
file:write(command)
file:close()
result = dt.control.execute(fname)
@@ -126,7 +118,6 @@ 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
@@ -137,4 +128,5 @@ function dtutils_system.launch_default_app(path)
return dtutils_system.external_command(open_cmd .. path)
end
return dtutils_system

View File

@@ -14,7 +14,7 @@ msgstr ""
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.5.4\n"
"Language: de_DE\n"

View File

@@ -1,514 +0,0 @@
--[[
apply_camera_style.lua - apply camera style to matching images
Copyright (C) 2024 Bill Ferguson <wpferguson@gmail.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
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 <wpferguson@gmail.com>
CHANGES
20251202 - fixed pattern matching bugs affecting Canon EOS R? series
styles and styles being applied twice if images are reimported
Added more debugging statements to pattern matching functions
]]
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 <const> = "apply_camera_style"
local DEFAULT_LOG_LEVEL <const> = log.warn
local TMP_DIR <const> = dt.configuration.tmp_dir
local STYLE_PREFIX <const> = "_l10n_darktable|_l10n_camera styles|"
local MAKER = 3
local STYLE = 4
-- path separator
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
-- command separator
local CS <const> = 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 <wpferguson@gmail.com>", -- 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)
log.msg(log.debug, "input pattern is " .. pattern)
pattern = string.lower(pattern)
log.msg(log.debug, "lower case pattern is " .. pattern)
-- strip off series
pattern = string.gsub(pattern, " series$", "?")
log.msg(log.debug, "series stripped off pattern is " .. pattern)
-- match a character
if string.match(pattern, "r%?%?$") then
-- handle EOS R case
pattern = string.gsub(pattern, "%?%?", ".?")
log.msg(log.debug, "matched EOS R pattern is " .. pattern)
else
pattern = string.gsub(pattern, "?", ".")
log.msg(log.debug, "didn't match EOS R pattern is " .. pattern)
end
-- escape dashes
pattern = string.gsub(pattern, "%-", "%%-")
log.msg(log.debug, "escape dashes conditional pattern is " .. pattern)
-- make spaces optional
pattern = string.gsub(pattern, " ", " ?")
-- until we end up with a set, I'll defer set processing, i.e. [...]
--log.msg(log.debug, "make spaces conditional again? pattern is " .. pattern)
-- anchor the pattern to ensure we don't short match
pattern = "^" .. pattern .. "$"
log.msg(log.debug, "anchored pattern is " .. pattern)
restore_log_level(log_level)
return pattern
end
local function process_set(pattern_set)
local log_level = set_log_level(acs.log_level)
local to_process = {}
local processed = {}
local base, set, tail
if string.match(pattern_set, "]$") then
base, set = string.match(pattern_set, "(.+)%[(.+)%]")
else
base, set, tail = string.match(pattern_set, "(.+)%[(.+)%](.+)")
end
log.msg(log.debug, "base is " .. base .. " and set is " .. set)
to_process = du.split(set, ",")
for _, item in ipairs(to_process) do
local pat = base .. item
if tail then
pat = pat .. tail
end
table.insert(processed, process_pattern(pat))
end
restore_log_level(log_level)
return processed
end
local function get_camera_styles()
local log_level = set_log_level(acs.log_level)
-- separate the styles into
--
-- acs.styles -
-- maker -
-- styles {}
-- patterns {}
for _, style in ipairs(dt.styles) do
if string.match(style.name, STYLE_PREFIX) then
log.msg(log.debug, "got " .. style.name)
local parts = du.split(style.name, "|")
parts[MAKER] = string.lower(parts[MAKER])
log.msg(log.debug, "maker is " .. parts[MAKER])
if not acs.styles[parts[MAKER]] then
acs.styles[parts[MAKER]] = {}
acs.styles[parts[MAKER]]["styles"] = {}
acs.styles[parts[MAKER]]["patterns"] = {}
end
if parts[STYLE] then
if not string.match(parts[STYLE], "]") then
table.insert(acs.styles[parts[MAKER]].styles, style)
local processed_pattern = process_pattern(parts[#parts])
table.insert(acs.styles[parts[MAKER]].patterns, processed_pattern)
log.msg(log.debug, "pattern for " .. style.name .. " is " .. processed_pattern)
else
local processed_patterns = process_set(parts[STYLE])
for _, pat in ipairs(processed_patterns) do
table.insert(acs.styles[parts[MAKER]].styles, style)
table.insert(acs.styles[parts[MAKER]].patterns, pat)
log.msg(log.debug, "pattern for " .. style.name .. " is " .. pat)
end
end
end
end
end
restore_log_level(log_level)
end
local function normalize_model(maker, model)
local log_level = set_log_level(acs.log_level)
model = string.lower(model)
-- strip off the maker name
if maker == "canon" then
model = string.gsub(model, "canon ", "")
elseif maker == "hasselblad" then
model = string.gsub(model, "hasselblad ", "")
elseif maker == "leica" then
model = string.gsub(model, "leica ", "")
elseif maker == "lg" then
model = string.gsub(model, "lg ", "")
elseif maker == "nikon" then
model = string.gsub(model, "nikon ", "")
elseif maker == "nokia" then
model = string.gsub(model, "nokia ", "")
elseif maker == "oneplus" then
model = string.gsub(model, "oneplus ", "")
elseif maker == "pentax" then
model = string.gsub(model, "pentax ", "")
model = string.gsub(model, "ricoh ", "")
end
restore_log_level(log_level)
return model
end
local function normalize_maker(maker)
local log_level = set_log_level(acs.log_level)
maker = string.lower(maker)
if string.match(maker, "^fujifilm") then
maker = "fujifilm"
elseif string.match(maker, "^hmd ") then
maker = "nokia"
elseif string.match(maker, "^leica") then
maker = "leica"
elseif string.match(maker, "^minolta") then
maker = "minolta"
elseif string.match(maker, "^nikon") then
maker = "nikon"
elseif string.match(maker, "^om ") then
maker = "om system"
elseif string.match(maker, "^olympus") then
maker = "olympus"
elseif string.match(maker, "^pentax") or string.match(maker, "^ricoh") then
maker = "pentax"
end
restore_log_level(log_level)
return maker
end
local function has_style_tag(image, tag_name)
local log_level = set_log_level(acs.log_level)
local result = false
log.msg(log.debug, "looking for tag " .. tag_name)
for _, tag in ipairs(image:get_tags()) do
log.msg(log.debug, "got attached tag " .. tag.name)
local cleaned_tag_name = string.gsub(tag_name, "_l10n_", "")
log.msg(log.debug, "checking against " .. cleaned_tag_name)
if ds.sanitize_lua(tag.name) == cleaned_tag_name then
log.msg(log.debug, "matched tag " .. cleaned_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, "mangle 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
log.msg(log.debug, "pattern is " .. pattern)
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])
log.msg(log.info, "applied style " .. acs.styles[maker].styles[i].name .. " to " .. image.filename)
end
no_match = false
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

View File

@@ -1,288 +0,0 @@
--[[
auto_straighten.lua - straighten an image in darkroom using image metadata
Copyright (C) 2025 Bill Ferguson <wpferguson@gmail.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
auto_straighten - straighten an image in darkroom using image metadata
auto_straighten reads the roll and pitch angle imformation captured when
an image is captured. A correction is computed and applied to correct the
roll to the nearest vertical or horizontal axis using the rotate and
perspective module.
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
exiftool - https://exiftool.org
USAGE
* add it sowewhere in your scripts directory
* ensure exiftool is installed and accessible
* enable it using script manager
BUGS, COMMENTS, SUGGESTIONS
Bill Ferguson <wpferguson@gmail.com>
CHANGES
LIMITATIONS
* Roll and pitch angles are taken when an image is captured. If you shoot
in burst mode, the roll and pitch angles of the first image are used in
the rest of the burst (at least for Canon).
* If you are running multiple scripts that trigger on an image being opened
in darkroom then the auto crop may not work. There is a shortcut included
that can have a key sequence assigned so that the auto crop will be reapplied.
* Currently oply roll angle is corrected
LIEENSE
GPL V2
]]
local dt = require "darktable"
local du = require "lib/dtutils"
-- local da = require "lib/dtutils.action"
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 <const> = "auto_straighten"
local DEFAULT_LOG_LEVEL <const> = log.info
local TMP_DIR <const> = dt.configuration.tmp_dir
-- path separator
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
-- command separator
local CS <const> = 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 = _("auto_straighten"), -- visible name of script
purpose = _("straighten an image in darkroom using image metadata"), -- purpose of script
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/auto_straighten/" -- 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_straighten = {}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- G L O B A L V A R I A B L E S
-- - - - - - - - - - - - - - - - - - - - - - - -
auto_straighten.pixelpipe_busy = false
-- - - - - - - - - - - - - - - - - - - - - - - -
-- P R E F E R E N C E S
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.preferences.register(MODULE, "crop_area", "enum",
_("automatic_cropping"),
_("what automatic cropping to perform"),
_("original format"),
_("off"), _("largest area"), _("original format"))
-- - - - - - - - - - - - - - - - - - - - - - - -
-- A L I A S E S
-- - - - - - - - - - - - - - - - - - - - - - - -
local namespace = auto_straighten
local as = auto_straighten
-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
local function determine_correction(roll_angle)
--[[
Canon camera roll angle is as foillows
* landscape orientation camera upright = 0
* camera rolled to the left (shutter button up) portrait = -90
* camera inverted landscape = +/- 180
* camera rolled to the right (shutter button down) portrait = 90
rotate and perspective corrections
* rotate the image to the right is a negative correction
* rotate the image left is a positive correction
]]
if roll_angle == 0 or -- landscape
roll_angle == -90 or -- roll left portrait
math.abs(roll_angle) == 180 or -- inverted landscape
roll_angle == 90 then -- roll right portrait
dt.print_log("on axis, no correction required")
return 0
elseif roll_angle > 0 and roll_angle <= 45 then
return (roll_angle * -1.0)
elseif roll_angle > 45 and roll_angle < 90 then
return (90 - roll_angle)
elseif roll_angle > 90 and roll_angle <= 135 then
return (roll_angle - 90) * -1.0
elseif roll_angle > 135 and roll_angle < 180 then
return (180 - roll_angle)
elseif roll_angle > -180 and roll_angle <= -135 then
return (-180 - roll_angle)
elseif roll_angle > -135 and roll_angle <= -90 then
return (math.abs(roll_angle) - 90)
elseif roll_angle > -90 and roll_angle <= -45 then
return (-90 - roll_angle)
else
return roll_angle * -1.0
end
end
local function toggle_auto_crop()
local crop = dt.preferences.read(MODULE, "crop_area", "enum")
dt.gui.action("iop/ashift/automatic cropping", "selection", "item:off", 1.000)
dt.control.sleep(100)
dt.gui.action("iop/ashift/automatic cropping", "selection", "item:" .. crop, 1.000)
end
local function straighten_image(image)
local roll = nil
local pitch = nil
-- Roll Angle 0 is landscape, -90 is rolled to the left in portrait, +- 180 is landscape inverted, 90 is portrait rolled to the right
local crop = dt.preferences.read(MODULE, "crop_area", "enum")
local pipe = io.popen(as.exiftool .. " -RollAngle -PitchAngle " .. image.path .. PS .. image.filename)
if pipe then
for line in pipe:lines() do
local pr, val = string.match(line, "(.).+: (.+)")
if pr == "R" then
local sign = 1.0
dt.print_log("roll angle is " .. val)
log.msg(log.debug, "roll angle is " .. val)
-- check for Nidon and change the sign
if string.match(string.upper(image.exif_maker), "NIKON") then
sign = -1.0
end
roll = determine_correction(tonumber(val) * sign)
dt.print_log("corrected roll angle is " .. roll)
log.msg(log.debug, "corrected roll angle is " .. roll)
elseif pr == "P" then
pitch = tonumber(val)
end
end
pipe:close()
end
if roll and dt.gui.action("iop/ashift", "enable", "", "") == 0.0 then
dt.print_log("straightening image")
dt.gui.action("iop/ashift/rotation", "value", "set", roll)
-- dt.gui.action("iop/ashift/automatic cropping", "selection", "item:" .. crop, 1.000)
-- da.wait_until_pixelpipe_complete()
toggle_auto_crop()
end
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N P R O G R A M
-- - - - - - - - - - - - - - - - - - - - - - - -
as.exiftool = df.check_if_bin_exists("exiftool")
if not as.exiftool then
log.msg(log.error, "exiftool not found, exiting...")
log.msg(log.screen, _("exiftool not found, exiting..."))
return
end
-- da.register_events(MODULE, straighten_image)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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, "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)
if clean then
straighten_image(image)
end
end
)
--]]
dt.register_event(MODULE, "shortcut",
function(event, shortcut)
toggle_auto_crop()
end, "toggle auto crop off and on"
)
return script_data

View File

@@ -34,23 +34,10 @@ 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

View File

@@ -27,25 +27,14 @@ USAGE
local dt = require "darktable"
local du = require "lib/dtutils"
local gettext = dt.gettext.gettext
local gettext = dt.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
@@ -69,6 +58,13 @@ 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
@@ -151,13 +147,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

View File

@@ -33,23 +33,10 @@ 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
@@ -72,7 +59,7 @@ local long_tags = {}
for _,t in ipairs(dt.tags) do
local len = #t.name
if len > max_length then
dt.print_log("deleting tag `"..t.name.."' (length: "..len..")")
print("deleting tag `"..t.name.."' (length: "..len..")")
table.insert(long_tags, t.name)
end
end

View File

@@ -37,23 +37,10 @@ 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
@@ -69,7 +56,7 @@ for _, t in ipairs(dt.tags) do
end
for _,name in pairs(unused_tags) do
dt.print_log("deleting tag `" .. name .. "'")
print("deleting tag `" .. name .. "'")
tag = dt.tags.find(name)
tag:delete()
end

View File

@@ -39,40 +39,36 @@ local dtsys = require "lib/dtutils.system"
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
local gettext = dt.gettext.gettext
local gettext = dt.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
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("enfuse",dt.configuration.config_dir..PS .. "lua" .. PS .. "locale" .. PS)
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
@@ -135,8 +131,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")
@@ -144,8 +140,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")
@@ -154,8 +150,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"
@@ -163,15 +159,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
@@ -189,7 +185,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
@@ -212,9 +208,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")
@@ -222,14 +218,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
@@ -255,7 +251,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
@@ -303,7 +299,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

View File

@@ -1,568 +0,0 @@
--[[
extract_burst_roll_images.lua - extract burst images from a burst roll file
Copyright (C) 2026 Bill Ferguson <wpferguson@gmail.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
extract_burst_roll_images - extract burst images from a burst roll file
Canon cameras are capable of shooting in burst mode. When the shutter
button is half pressed the camera starts "recording". When the shutter is
fully pressed the pervious 1/2 second of imformation is recorded as individual
images stored in one file as well as images captured at 30 frames/sec until
the buffer fills or the shutter button is released. So a Canon R7 burst file
could contain ~90 images.
Extracting the images from this file required use of Canon proprietary software
until recently when dnglab got the capability to extract the embedded images as
DNG raws.
This script can be set to run on import, scanning for burst roll containers,
extracting the embedded images and grouping them with the burst roll container.
The script can alsu be utilized via a short cut and applied to selected images.
When set to on import the script scans each image after import using exiv2 or exiftool
to check the image EXIF information to see if the file is a busrt roll container.
Once the import images are scanned, the detected burst roll containers are processed,
the embedded images extracted as DNSs, and grouped with the container image.
In shortcut mode the user selects the burst roll containers manually and uses the shortcut
to extract the embedded images as DNGs
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
- exiv2 - https://exiv2.org
- exiftool - https://exiftool.org
- dnglab - https://github.com/dnglab/dnglab
USAGE
- add the script to your lua scripts
- enable in script manager
- set the preferences in perferences->Lua options
LIMITATIONS
extract_burst_roll_images makes heavy use of external programs to identify burst roll
containters and to extract the images. Windows users may want to use it in shortcut mode
to lessen the number of windows popping up.
BUGS, COMMENTS, SUGGESTIONS
Bill Ferguson <wpferguson@gmail.com>
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"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- A P I C H E C K
-- - - - - - - - - - - - - - - - - - - - - - - -
du.check_min_api_version("9.6.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 = _("extract burst roll images"), -- visible name of script
purpose = _("extract burst images from a burst roll file"), -- purpose of script
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/extract_burst_roll_images/" -- URL to help/documentation
}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
local MODULE <const> = "extract_burst_roll_images"
local DEFAULT_LOG_LEVEL <const> = log.info
local TMP_DIR <const> = dt.configuration.tmp_dir
-- path separator
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
-- command separator
local CS <const> = dt.configuration.running_os == "windows" and "&" or ";"
local EXIFTOOL <const> = df.check_if_bin_exists("exiftool")
local EXIV2 <const> = df.check_if_bin_exists("exiv2")
local DNGLAB <const> = df.check_if_bin_exists("dnglab")
local EXIV2TAG <const> = "Exif.Canon.RawBurstModeRoll"
local HAS_INTERNAL_METADATA <const> = dt.metadata.exists(EXIV2TAG)
local DNGLAB_ARGS <const> = " convert --image-index all --embed-raw false "
local EXIV2_ARGS <const> = " -K Exif.Canon.RawBurstModeRoll -pt "
local EXIFTOOL_ARGS <const> = " -T -RawBurstImageCount "
-- - - - - - - - - - - - - - - - - - - - - - - -
-- L O G L E V E L
-- - - - - - - - - - - - - - - - - - - - - - - -
log.log_level(DEFAULT_LOG_LEVEL)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- N A M E S P A C E
-- - - - - - - - - - - - - - - - - - - - - - - -
local extract_burst_roll_images = {}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- G L O B A L V A R I A B L E S
-- - - - - - - - - - - - - - - - - - - - - - - -
extract_burst_roll_images.preferred_tag_reader = nil
extract_burst_roll_images.on_import = true
extract_burst_roll_images.select_button = true
extract_burst_roll_images.imported_images = {}
extract_burst_roll_images.log_level = DEFAULT_LOG_LEVEL
-- - - - - - - - - - - - - - - - - - - - - - - -
-- P R E F E R E N C E S
-- - - - - - - - - - - - - - - - - - - - - - - -
-- exiv2 or exiftool
dt.preferences.register(MODULE, -- 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.
"tag_reader", -- name
"enum", -- type
_("preferred exif tag reader"), -- label
_("use exiftool or exiv2 to read exif tags"), -- tooltip
"exiftool", -- default
"exiftool", "exiv2") -- values
-- run on import
dt.preferences.register(MODULE, -- 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.
"on_import", -- name
"bool", -- type
_("extract burst roll images on import"), -- label
_("extract burst roll images on import"), -- tooltip
false) -- default
-- display selection button
dt.preferences.register(MODULE, -- 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.
"select_button", -- name
"bool", -- type
_("display select burst roll images button"), -- label
_("add a button to the select module to select burst roll images"), -- tooltip
true) -- default
-- - - - - - - - - - - - - - - - - - - - - - - -
-- A L I A S E S
-- - - - - - - - - - - - - - - - - - - - - - - -
local namespace = extract_burst_roll_images
local ebri = extract_burst_roll_images
local sleep = dt.control.sleep
-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
-------------------
-- helper functions
-------------------
local function set_log_level(level)
if not level then
level = namespace.log_level
end
local old_log_level = log.log_level()
log.log_level(level)
return old_log_level
end
local function restore_log_level(level)
if not level then
level = namespace.log_level
end
log.log_level(level)
end
local function reset_log_level()
log.log_level(DEFAULT_LOG_LEVEL)
namespace.log_level = DEFAULT_LOG_LEVEL
end
local function pref_read(name, pref_type)
local old_log_level = set_log_level(ebri.log_level)
log.msg(log.debug, "name is " .. name .. " and type is " .. pref_type)
local val = dt.preferences.read(MODULE, name, pref_type)
log.msg(log.debug, "read value " .. tostring(val))
restore_log_level(old_log_level)
return val
end
local function pref_write(name, pref_type, value)
local old_log_level = set_log_level(ebri.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
local function get_last_value(valuestr)
local old_log_level = set_log_level()
local parts = du.split(valuestr, " +")
if parts then
log.msg(log.debug, "last value of parts is " .. tostring(parts[#parts]))
return parts[#parts]
else
log.msg(log.debug, "last part not detected, returning nil")
return nil
end
restore_log_level(old_log_level)
end
local function is_burst_roll_image(image)
local old_log_level = set_log_level()
local result = false
if HAS_INTERNAL_METADATA then
if image.RawBurstModeRoll then
local val = get_last_value(image.RawBurstModeRoll)
val = tonumber(val)
if type(val) == "number" then
if val > 0 then
result = true
end
end
end
else
local cmd = EXIFTOOL .. EXIFTOOL_ARGS
if ebri.preferred_tag_reader == "exiv2" then
cmd = EXIV2 .. EXIV2_ARGS
end
local p = io.popen(cmd .. ds.sanitize(image.path) .. PS .. ds.sanitize(image.filename))
if p then
for line in p:lines() do
if ebri.preferred_tag_reader == "exiv2" then
line = get_last_value(line)
end
line = tonumber(line)
if type(line) == "number" then
if line > 0 then
log.msg(log.info, image.filename .. " is a burst roll container image")
result = true
end
end
end
p:close()
end
end
restore_log_level(old_log_level)
return result
end
local function group_extracted_dngs(image)
local old_log_level = set_log_level()
local count = 0
local cmd = "ls -1 " .. ds.sanitize(image.path) .. PS .. ds.sanitize(ds.get_basename(image.filename)) .. "*.dng"
if dt.configuration.running_os == "windows" then
cmd = "forfiles /P " .. ds.sanitize(image.path) .. " /M " .. ds.sanitize(ds.get_basename(image.filename)) .. "*.dng" .. " /C \"cmd /c echo @path\\@fname\""
end
local p = io.popen(cmd)
if p then
for line in p:lines() do
if line:len() > 5 then -- skip blank lines created by forfiles
local dng_image = dt.database.import(line)
dng_image:group_with(image)
count = count + 1
end
end
p:close()
log.msg(log.screen, string.format(_("extracted %d dng files from %s"), count, image.filename))
end
restore_log_level(old_log_level)
end
local function extract_burst(image)
local old_log_level = set_log_level()
local cmd = DNGLAB .. DNGLAB_ARGS .. ds.sanitize(image.path) .. PS .. ds.sanitize(image.filename) .. " " .. ds.sanitize(image.path)
local result = dtsys.external_command(cmd)
if result == 0 then
group_extracted_dngs(image)
end
restore_log_level(old_log_level)
end
local function extract_burst_images(images)
for _, image in ipairs(images) do
if is_burst_roll_image(image) then
extract_burst(image)
end
end
end
local function process_image(image)
local old_log_level = set_log_level()
if is_burst_roll_image(image) then
extract_burst(image)
end
restore_log_level(old_log_level)
end
local function stop_job(job)
job.valid = false
end
local function process_images(images)
local old_log_level = set_log_level()
local job = dt.gui.create_job(_("scanning for burst roll images"), true, stop_job)
for i, image in ipairs(images) do
if ebri.job.valid then
extract_burst(image)
ebri.job.percent = i / #images
sleep(10)
end
end
stop_job(job)
restore_log_level(old_log_level)
end
local function process_import(images)
local old_log_level = set_log_level()
local burst_roll_images = {}
local job = dt.gui.create_job(_("scanning for burst roll images"), true, stop_job)
for i, image in ipairs(images) do
if job.valid then
if is_burst_roll_image(image) then
table.insert(burst_roll_images, image)
end
if i % 10 == 0 then
job.percent = i / #images
end
sleep(10)
end
end
stop_job(job)
if #burst_roll_images > 0 then
job = dt.gui.create_job(_("extracting burst roll images"), true, stop_job)
for j, image in ipairs(burst_roll_images) do
if job.valid then
extract_burst(image)
job.percent = j / #burst_roll_images
sleep(10)
end
end
stop_job(job)
end
restore_log_level(old_log_level)
end
local function select_burst_roll_images(event, images)
local old_log_level = set_log_level()
local selected = {}
for _, image in ipairs(images) do
if is_burst_roll_image(image) then
table.insert(selected, image)
end
end
restore_log_level(old_log_level)
return selected
end
local function set_select_sensitive()
if #dt.collection > 0 then
dt.gui.libs.select.set_sensitive(MODULE, true)
else
dt.gui.libs.select.set_sensitive(MODULE, false)
end
end
local function set_action_sensitive()
local sensitive = false
if #dt.gui.action_images > 0 then
if is_burst_roll_image(dt.gui.action_images[1]) then
sensitive = true
end
end
dt.gui.libs.image.set_sensitive(MODULE, sensitive)
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N P R O G R A M
-- - - - - - - - - - - - - - - - - - - - - - - -
if not DNGLAB then
log.msg(log.error, MODULE .. _("dnglab executable not found"))
log.msg(log.screen, script_data.metadata.name .. _(": dnglab executable not found, exiting..."))
return
end
if HAS_INTERNAL_METADATA then
log.msg(log.info, "Exif.Canon.RawBurstModeRoll detected, using internal metadata")
else
if not EXIFTOOL and not EXIV2 then
log.msg(log.error, MODULE .. _("no exif tag reader available"))
log.msg(log.screen, script_data.metadata.name .. _(" no exif tag reader available, exiting..."))
return
end
ebri.preferred_tag_reader = pref_read("tag_reader", "enum")
if not string.match(EXIFTOOL, ebri.preferred_tag_reader) and not string.match(EXIV2, ebri.preferred_tag_reader) then
log.msg(log.error, MODULE .. " selected tag " .. ebri.preferred_tag_reader .." not available")
log.msg(log.screen, script_data.metadata.name .. string.format(_(" selected tag reader %s not available"), ebri.preferred_tag_reader))
return
end
end
ebri.on_import = pref_read("on_import", "bool")
ebri.select_button = pref_read("select_button", "bool")
-- - - - - - - - - - - - - - - - - - - - - - - -
-- U S E R I N T E R F A C E
-- - - - - - - - - - - - - - - - - - - - - - - -
if ebri.select_button then
dt.gui.libs.select.register_selection(MODULE, _("select burst roll images"), select_burst_roll_images,
_("select burst roll image containers"))
end
dt.gui.libs.image.register_action(
MODULE,
_("extract burst roll images"),
function(event, images)
extract_burst_images(images)
end,
_("extract and group burst roll images with container")
)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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-film")
dt.destroy_event(MODULE, "post-import-image")
if ebri.select_button then
dt.gui.libs.select.destroy_selection(MODULE)
end
dt.gui.libs.image.action.destroy_action(MODULE)
end
script_data.destroy = destroy
-- - - - - - - - - - - - - - - - - - - - - - - -
-- E V E N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.register_event(MODULE, "post-import-film",
function(event, film)
process_import(ebri.imported_images)
ebri.imported_images = {}
end
)
dt.register_event(MODULE, "post-import-image",
function(event, image)
if ebri.on_import then
table.insert(ebri.imported_images, image)
end
end
)
dt.register_event(MODULE, "selection-changed",
function(event)
set_action_sensitive()
end
)
if dt.preferences.read("darktable", "plugins/lighttable/act_on", "bool") then
dt.print_log("adding mouse-over-image-changed event")
dt.register_event(MODULE, "mouse-over-image-changed",
function(event, image)
set_action_sensitive()
end
)
end
dt.register_event(MODULE, "collection-changed",
function(event)
set_select_sensitive()
end
)
if not dt.query_event(MODULE, "shortcut") then
dt.register_event(MODULE, "shortcut",
function(event, shortcut)
if #dt.gui.action_images > 1 then
process_images(dt.gui.action_images)
else
process_image(dt.gui.action_images[1])
end
end, "extract roll burst images"
)
end
return script_data

View File

@@ -38,25 +38,12 @@ 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
@@ -65,21 +52,21 @@ script_data.show = nil -- only required for libs since the destroy_method only h
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

View File

@@ -1,694 +0,0 @@
--[[
group_persistence.lua - preserve groups between darktable instances
Copyright (C) 2024-2026 Bill Ferguson <wpferguson@gmail.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
group_persistence - preserve groups between darktable instances
group_persistence uses "functional" tagging to maintain image groups
across database instances, allowing image grouping to be restored when
the database is rebuilt from the XMP sidecar files.
Once the script is started it "listens" for changes in image grouping and
applies or removes the necessary tags automatically.
A shortcut can be assigned to read a collection and apply the necessary
grouping tags so that existing collections can be maintained.
If the database is lost and needs to be rebuilt then start the script before
importing any images. As images are imported the tags will be read and the
grouping applied in the database.
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
None
USAGE
* Enable the script using script manager
* Optionally, create a keystroke combination for the shortcut
LIMITATIONS
In normal use you wont notice that the script is running.
However if you do something like import 4000+ images, then
run the autogrouper script to create groups whenever there is
a 5 second difference between 2 images, your system will be busy
for awhile. If for some reason you need to do something like this
turn off group_persistence, run the autogrouper, turn on group_persistence
and use the shortcut to read all the grouping information and apply
the necessary tags. It will still be an impact, but less than trying
to do both at once. Learned from experience :-)
BUGS, COMMENTS, SUGGESTIONS
Bill Ferguson <wpferguson@gmail.com>
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 <const> = "group_persistence"
local DEFAULT_LOG_LEVEL <const> = log.error
local TMP_DIR <const> = dt.configuration.tmp_dir
-- path separator
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
-- command separator
local CS <const> = dt.configuration.running_os == "windows" and "&" or ";"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- A P I C H E C K
-- - - - - - - - - - - - - - - - - - - - - - - -
du.check_min_api_version("9.4.0", MODULE) -- choose the minimum version that contains the features you need
-- - - - - - - - - - - - - - - - - - - - - - - - - -
-- I 1 8 N
-- - - - - - - - - - - - - - - - - - - - - - - - - -
local gettext = dt.gettext.gettext
dt.gettext.bindtextdomain(MODULE , dt.configuration.config_dir .. "/lua/locale/")
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 = _("group persistence"), -- name of script
purpose = _("preserve groups between darktable instances"), -- purpose of script
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/group_persistence" -- 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 group_persistence = {}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- A L I A S E S
-- - - - - - - - - - - - - - - - - - - - - - - -
local namespace = group_persistence
local gp = group_persistence
-- - - - - - - - - - - - - - - - - - - - - - - -
-- G L O B A L V A R I A B L E S
-- - - - - - - - - - - - - - - - - - - - - - - -
-- table of imported images that need grouping rebuilt
gp.imported_images = {}
-- uuid to group leader mapping
gp.uuids = {}
-- group leader to uuid mapping
gp.leaders = {}
-- uuid to group tag mapping
gp.group_tags = {}
-- uuid to group leader tag mapping
gp.leader_tags = {}
-- when the groups are rebuilt after import they
-- cause the group change events to fire but the
-- events are queued until after rebuilding completes
-- so we need to build a table of images to ignore when
-- the queued events catch up. As the event fires for an
-- image, the image is removed.
gp.rebuilt_images = {}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- P R E F E R E N C E S
-- - - - - - - - - - - - - - - - - - - - - - - -
-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
-- utilities for building/debugging
local function dump_image_tags(image)
log.msg(log.debug, "")
for _, tag in ipairs(image:get_tags()) do
log.msg(log.debug, "tag for image " .. image.id .. " is " .. tag.name)
end
log.msg(log.debug, "group_leader is " .. image.group_leader.id .. " and group has " .. #image:get_group_members() .. " members")
log.msg(log.debug, "")
end
local function dump_group_tags(image)
local leader = image.group_leader
for _, tag in ipairs(image:get_tags()) do
if string.match(tag.name, "darktable|group") then
log.msg(log.debug, "group leader image " .. image.id .. " has tag " .. tag.name)
end
end
for _, img in ipairs(image:get_group_members()) do
for _, tag in ipairs(img:get_tags()) do
if string.match(tag.name, "darktable|group") then
log.msg(log.debug, "group member image " .. img.id .. " has tag " .. tag.name)
end
end
end
end
-- rebuild grouping on import
local function save_group_leader(image, uuid)
log.log_level(DEFAULT_LOG_LEVEL)
log.msg(log.debug, "input image is " .. image.id .. " and uuid is " .. uuid)
gp.uuids[image] = uuid
gp.leaders[uuid] = image
log.msg(log.debug, "after saving group leader uuid is " .. gp.leaders[uuid].id .. " and image is " .. gp.uuids[image])
end
local function get_group_and_uuid(tags)
log.log_level(DEFAULT_LOG_LEVEL)
local group_type = nil
local uuid = nil
for _, tag in ipairs(tags) do
if string.match(tag.name, "darktable|group") then
log.msg(log.debug, "found tag darktable|group")
local parts = du.split(tag.name, "|")
group_type = parts[2]
uuid = parts[3]
log.msg(log.info, "found type " .. group_type .. " with uuid " .. uuid)
end
end
return group_type, uuid
end
local function get_uuid_from_tag(tag)
local parts = du.split(tag.name, "|")
return parts[#parts]
end
local function find_group_leader(uuid)
local group_leader = nil
local tag_name = "darktable|group_leader|" .. uuid
local found_tag = dt.tags.find(tag_name)
if found_tag then
group_leader = dt.tags.get_tagged_images(found_tag)[1]
end
return group_leader
end
local function rebuild_groups()
log.log_level(DEFAULT_LOG_LEVEL)
for _, image in ipairs(gp.imported_images) do
log.msg(log.info, "processing image " .. image.id)
local tags = image:get_tags()
local group_type, uuid = get_group_and_uuid(tags)
if group_type =="group_leader" then
table.insert(gp.rebuilt_images, image)
log.msg(log.debug, "took group_leader branch")
save_group_leader(image, uuid)
elseif group_type == "group" then
table.insert(gp.rebuilt_images, image)
log.msg(log.debug, "took group branch")
local leader = gp.uuids[uuid]
if leader then
image:group_with(leader)
else
local group_leader = find_group_leader(uuid)
if group_leader then
save_group_leader(group_leader, uuid)
image:group_with(group_leader)
end
end
end
end
gp.imported_images = {}
end
-- rebuild grouping on command (shortcut)
local function is_group_leader(image)
result = false
if image.group_leader == image and #image:get_group_members() > 1 then
result = true
end
return result
end
local function has_group_tag(image)
local result = false
for _, tag in ipairs(image:get_tags()) do
if string.match(tag.name, "darktable|group|") then
result = tag
end
end
return result
end
local function has_group_leader_tag(image)
local result = false
for _, tag in ipairs(image:get_tags()) do
if string.match(tag.name, "darktable|group_leader") then
result = tag
end
end
return result
end
-- handle grouping changes in the UI
local function group_exists(group_leader)
local exists = false
local tags = group_leader:get_tags()
for _, tag in ipairs(tags) do
if string.match(tag.name, "group_leader") then
exists = true
break
end
end
return exists
end
local function add_group_leader(image, group)
-- tag image as group leader
-- create the uuid and leader/group tags if necessary
log.log_level(DEFAULT_LOG_LEVEL)
local uuid = du.gen_uuid()
if uuid then
log.msg(log.debug, "in add_group_leader and uuid is " .. uuid)
else
log.msg(log.debug, "uuid not created")
end
local tag = dt.tags.create("darktable|group_leader|" .. uuid)
local group_tag = dt.tags.create("darktable|group|" .. uuid)
log.msg(log.info, "created group leader tag " .. tag.name)
log.msg(log.info, "created group tag " .. group_tag.name)
gp.group_tags[uuid] = group_tag
gp.leader_tags[uuid] = tag
gp.uuids[uuid] = image
gp.leaders[image] = uuid
image:attach_tag(tag)
log.msg(log.info, "attached tag " .. tag.name .. " to image " .. image.id)
end
local function add_to_group(image, group_leader)
-- add an image to a group
-- create the group if necessary
log.log_level(DEFAULT_LOG_LEVEL)
if image == group_leader then
add_group_leader(image, group_leader)
else
if not group_exists(group_leader) then
add_group_leader(group_leader, group_leader)
end
local group_tag = nil
log.msg(log.debug, "image is " .. image.id .. " and group_leader is " .. image.group_leader.id)
local group, uuid = get_group_and_uuid(image.group_leader:get_tags())
group_tag = gp.group_tags[uuid]
if not group_tag then
log.msg(log.info, "creating group tag darktable|group|" .. uuid)
group_tag = dt.tags.create("darktable|group|" .. uuid)
gp.group_tags[uuid] = group_tag
end
image:attach_tag(group_tag)
log.msg(log.info, "group tag attached to image " .. image.id)
end
end
local function build_group(leader)
if not has_group_leader_tag(leader) then
add_group_leader(leader)
end
for _, image in ipairs(leader:get_group_members()) do
if not has_group_tag(image) then
add_to_group(image, leader)
end
end
end
local function process(image_set)
local images
if image_set == "collection" then
images = dt.collection
else
images = dt.gui.selection()
if #images == 0 then
images = dt.gui.action_images
end
end
for _, image in ipairs(images) do
if is_group_leader(image) then
build_group(image)
end
end
end
local function check_and_remove_empty_group_data(group_tag)
if not group_tag then
return -- handle duplicate getting deleted
end
local group_leader_tag = nil
group_leader_tag = dt.tags.find(string.gsub(group_tag.name, "|group|", "|group_leader|"))
local group_leader_count = 0
local group_count = #dt.tags.get_tagged_images(group_tag)
if group_leader_tag then
group_leader_count = #dt.tags.get_tagged_images(group_leader_tag)
end
if group_leader_count + group_count < 2 then -- there's either 1 or 0, so no group
local uuid = get_uuid_from_tag(group_tag)
if gp.group_tags[uuid] then
gp.group_tags[uuid] = nil
end
if gp.uuids[image] then
gp.uuids[image] = nil
end
if gp.leaders[uuid] then
gp.leaders[uuid] = nil
end
group_tag:delete()
if group_leader_tag then
group_leader_tag:delete()
end
end
end
local function remove_group_leader(image, new_group_leader)
-- remove image group leader tag
-- if it's the last image, then remove the leader/group tags
log.log_level(DEFAULT_LOG_LEVEL)
dump_image_tags(image)
local group, uuid = get_group_and_uuid(image:get_tags())
local leader_tag = dt.tags.find("darktable|group_leader|" .. uuid)
local group_tag = dt.tags.find("darktable|group|" .. uuid)
image:detach_tag(leader_tag)
gp.leaders[image] = nil
log.msg(log.info, "removed image " .. image.id .. " as group leader for uuid " .. uuid)
new_group_leader:detach_tag(group_tag)
new_group_leader:attach_tag(leader_tag)
log.msg(log.info, "added image " .. new_group_leader.id .. " as group leader for uuid " .. uuid)
gp.leaders[new_group_leader] = uuid
gp.uuids[uuid] = new_group_leader
check_and_remove_empty_group_data(group_tag)
end
local function remove_from_group(image, group_leader)
-- remove an image from a group
-- if it's the last image then remove the tags
-- image.id > group_leder.id - simple remove
-- image.id < group_leader.id - deleting old group_leader (image) with new group_leader (group_leader)
log.log_level(DEFAULT_LOG_LEVEL)
if image.id < group_leader.id then
remove_group_leader(image, group_leader)
else
local group_tag = has_group_tag(image)
if group_tag then
image:detach_tag(group_tag)
log.msg(log.info, "image " .. image.filename .. " with id " .. image.id .. " removed from group " .. group_tag.name)
end
check_and_remove_empty_group_data(group_tag)
end
end
local function change_group_leader(image)
-- remove the group leader tag from the old leader
-- replace the old group leader tag with a group tag
-- add the group leader tag to the new image
log.log_level(DEFAULT_LOG_LEVEL)
dump_image_tags(image)
local group, uuid = get_group_and_uuid(image:get_tags())
local leader_tag = dt.tags.find("darktable|group_leader|" .. uuid)
local group_tag = dt.tags.find("darktable|group|" .. uuid)
old_leader = dt.tags.get_tagged_images(leader_tag)[1]
old_leader:detach_tag(leader_tag)
log.msg(log.info, "remove leader tag from image " .. old_leader.id)
old_leader:attach_tag(group_tag)
log.msg(log.info, "attached group tag to image " .. old_leader.id)
image:detach_tag(group_tag)
log.msg(log.info, "removed group tag from new leader image " .. image.id)
image:attach_tag(leader_tag)
log.msg(log.info, "attached leader tag to new leader image " .. image.id)
gp.leaders[uuid] = image
gp.uuids[image] = uuid
end
-- event handling
local function add_group_event()
dt.register_event(MODULE, "image-group-information-changed",
function(event, reason, image, other_image)
log.msg(log.debug, "")
log.msg(log.debug, "caught event with reason " .. reason .. " and image " .. image.id .. " and other image " .. other_image.id)
log.msg(log.debug, "")
for pos, rimage in ipairs(gp.rebuilt_images) do
if rimage == image then
table.remove(gp.rebuilt_images, pos)
return
end
end
if reason == "add" then
log.msg(log.debug, "in add, image is " .. image.id .. " and group is " .. other_image.id)
if image == other_image then
add_group_leader(image, other_image)
else
add_to_group(image, other_image)
end
elseif reason == "remove" then
log.msg(log.debug, "in remove with image " .. image.id .. " and group_id " .. other_image.id)
remove_from_group(image, other_image)
elseif reason == "remove-leader" then
remove_group_leader(image, other_image)
log.msg(log.debug, "in remove-leader with image " .. image.id .. " and new group id " .. other_image.id)
else
if image ~= other_image then
log.msg(log.debug, "in change-leader with image " .. image.id .. " and other_id " .. other_image.id)
change_group_leader(image)
log.msg(log.debug, "in change-leader with image " .. image.id .. " and other_id " .. other_image.id)
else
log.msg(log.debug, "in change-leader with image " .. image.id .. " and other_id " .. other_image.id)
log.msg(log.debug, "calling add_group_leader")
add_group_leader(image, other_image)
end
end
end
)
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N P R O G R A M
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.util.message(MODULE, "autogrouper", "running")
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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")
dt.destroy_event(MODULE, "image-group-information-changed")
end
script_data.destroy = destroy
-- - - - - - - - - - - - - - - - - - - - - - - -
-- E V E N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.register_event(MODULE, "post-import-image",
function(event, image)
table.insert(gp.imported_images, image)
end
)
dt.register_event(MODULE, "post-import-film",
function(event, filmroll)
rebuild_groups()
end
)
add_group_event()
dt.register_event(MODULE, "shortcut",
function(event, string)
dump_image_tags(dt.gui.action_images[1])
end, "dump image tags"
)
dt.register_event(MODULE, "shortcut",
function(event, string)
dump_group_tags(dt.gui.action_images[1])
end, "dump group_tags"
)
dt.register_event(MODULE, "shortcut",
function(event, string)
process("collection")
end, "group collection"
)
return script_data

View File

@@ -33,23 +33,10 @@ 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
@@ -63,7 +50,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
)

View File

@@ -32,40 +32,20 @@ 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"),
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)
@@ -95,12 +75,6 @@ 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;

View File

@@ -30,31 +30,11 @@ 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_log("ignoring all jpegs")
dt.print_error("ignoring all jpegs")
for i, img in ipairs(images) do
local extension = img:match("[^.]*$"):upper()
if (extension == "JPG") or (extension == "JPEG") then
@@ -108,13 +88,5 @@ 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;

View File

@@ -1,343 +0,0 @@
--[[
recent_bookmarks.lua - add recently edited and exported images to system recent bookmarks
Copyright (C) 2026 Bill Ferguson <wpferguson@gmail.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
recent_bookmarks - add recently edited and exported images to system recently used files
recent_bookmarks adds images edited in darktable and the directory of images exported from
darktable to the reccently-used.xbel file so that they show up in the recently used system
shortcuts.
Currently recent_bookmarks only works on Linux
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
file - linux system file utility to get mime-type
USAGE
eanble from script manager
BUGS, COMMENTS, SUGGESTIONS
Bill Ferguson <wpferguson@gmail.com>
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"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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 = _("recent_bookmarks"), -- visible name of script
purpose = _("add recently edited and exported images to system recent bookmarks"), -- purpose of script
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
help = "" -- URL to help/documentation
}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
local MODULE <const> = "recent_bookmarks"
local DEFAULT_LOG_LEVEL <const> = log.info
local TMP_DIR <const> = dt.configuration.tmp_dir
-- path separator
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
-- command separator
local CS <const> = dt.configuration.running_os == "windows" and "&" or ";"
-- format strings for writing the bookmarks
local BOOKMARK_FORMAT <const> = " <bookmark href=\"file://%s\" added=\"%s\" modified=\"%s\" visited=\"%s\">\n"
local MIME_FORMAT <const> = " <mime:mime-type type=\"%s\"/>\n"
local EXPORT_FORMAT <const> = " <bookmark:application name=\"xdg-open\" exec=\"&apos;xdg-open %%u&apos;\" modified=\"%s\" count=\"1\"/>\n"
local EDIT_FORMAT <const> = " <bookmark:application name=\"darktable\" exec=\"&apos;darktable %%u&apos;\" modified=\"%s\" count=\"1\"/>\n"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- L O G L E V E L
-- - - - - - - - - - - - - - - - - - - - - - - -
log.log_level(DEFAULT_LOG_LEVEL)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- N A M E S P A C E
-- - - - - - - - - - - - - - - - - - - - - - - -
local recent_bookmarks = {}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- G L O B A L V A R I A B L E S
-- - - - - - - - - - - - - - - - - - - - - - - -
recent_bookmarks.log_level = DEFAULT_LOG_LEVEL
recent_bookmarks.bookmark_file = nil
recent_bookmarks.current_view = nil
recent_bookmarks.in_darkroom = false
recent_bookmarks.current_image = nil
-- - - - - - - - - - - - - - - - - - - - - - - -
-- P R E F E R E N C E S
-- - - - - - - - - - - - - - - - - - - - - - - -
-- - - - - - - - - - - - - - - - - - - - - - - -
-- A L I A S E S
-- - - - - - - - - - - - - - - - - - - - - - - -
local namespace = recent_bookmarks
local rb = recent_bookmarks
-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
-------------------
-- helper functions
-------------------
local function set_log_level(level)
if not level then
level = namespace.log_level
end
local old_log_level = log.log_level()
log.log_level(level)
return old_log_level
end
local function restore_log_level(level)
if not level then
level = namespace.log_level
end
log.log_level(level)
end
local function reset_log_level()
log.log_level(DEFAULT_LOG_LEVEL)
namespace.log_level = DEFAULT_LOG_LEVEL
end
local function pref_read(name, pref_type)
local old_log_level = set_log_level(namespace.log_level)
log.msg(log.debug, "name is " .. name .. " and type is " .. pref_type)
local val = dt.preferences.read(MODULE, name, pref_type)
log.msg(log.debug, "read value " .. tostring(val))
restore_log_level(old_log_level)
return val
end
local function pref_write(name, pref_type, value)
local old_log_level = set_log_level(namespace.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
--------------------
-- program functions
--------------------
local function zulu_timestamp()
return os.date("!%Y-%m-%dT%H:%M:%SZ")
end
local function get_mimetype(filepath)
local mimetype = "application/octet-stream"
local p = io.popen("file -L --mime-type " .. df.sanitize_filename(filepath))
if p then
for line in p:lines() do
local result = string.match(line, ".-: (.+)$")
if result then
mimetype = result
end
end
end
return mimetype
end
local function find_bookmark_file()
local file = nil
local home = os.getenv("HOME")
if home then
if df.test_file(home .. PS ..".local/share/recently-used.xbel", "e") then
file = home .. PS ..".local/share/recently-used.xbel"
end
end
return file
end
local function write_bookmark_file(bookmark, mimetype, application)
local file = io.open(rb.bookmark_file, "r+")
if file then
local pos = file:seek("end", -7)
if pos > 0 then
file:write(bookmark)
file:write(" <info>\n <metadata owner=\"http://freedesktop.org\">\n")
file:write(mimetype)
file:write(" <bookmark:applications>\n")
file:write(application)
file:write(" </bookmark:applications>\n </metadata>\n </info>\n </bookmark>\n</xbel>")
file:close()
else
log.msg(log.screen, _("failed to open bookmark file"))
end
else
log.msg(log.screen, _("failed to open bookmark file"))
end
end
local function create_bookmark_string(filename, zulu)
return string.format(BOOKMARK_FORMAT, filename, zulu, zulu, zulu)
end
local function create_mimetype_string(mimetype)
return string.format(MIME_FORMAT, mimetype)
end
local function process_bookmark(image, filename)
local zulu = zulu_timestamp()
local filepath = filename and filename or (image.path .. PS .. image.filename)
local format = filename and EXPORT_FORMAT or EDIT_FORMAT
local mimetype = get_mimetype(filepath)
local bookmark_string = create_bookmark_string(filepath, zulu)
local mime_string = create_mimetype_string(mimetype)
local app_string = string.format(format, zulu)
write_bookmark_file(bookmark_string, mime_string, app_string)
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N P R O G R A M
-- - - - - - - - - - - - - - - - - - - - - - - -
if dt.configuration.running_os ~= "linux" then
log.msg(log.screen, script_data.metadata.name .. _(" only runs on Linux"))
return
end
rb.bookmark_file = find_bookmark_file()
if not rb.bookmark_file then
log.msg(log.screen, _("recent bookmarks file not found"))
return
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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, "intermediate-export-image")
dt.destroy_event(MODULE, "darkroom-image-loaded")
dt.destroy_event(MODULE, "darkroom-image-history-changed")
dt.destroy_event(MODULE, "view-changed")
end
script_data.destroy = destroy
-- - - - - - - - - - - - - - - - - - - - - - - -
-- E V E N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.register_event(MODULE, "intermediate-export-image",
function(event, image, filename, format, storage)
process_bookmark(image, filename)
end
)
dt.register_event(MODULE, "darkroom-image-loaded",
function(event, clean, image)
if clean then
rb.in_darkroom = true
rb.current_view = "darkroom"
rb.current_image = image
end
end
)
dt.register_event(MODULE, "darkroom-image-history-changed",
function(event, image)
if image == rb.current_image then
process_bookmark(image)
end
end
)
dt.register_event(MODULE, "view-changed",
function(event, old_view, new_view)
rb.current_view = new_view.id
if rb.current_view == "darkroom" then
rb.in_darkroom = true
else
if rb.in_darkroom then
rb.in_darkroom = false
end
end
end
)
return script_data

View File

@@ -1,267 +0,0 @@
--[[
regenerate_thumbnails.lua - regenerate mipmap cache for selected images
Copyright (C) 2025 Bill Ferguson <wpferguson@gmail.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
regenerate_thumbnails - regenerate mipmap cache for selected images
regenerate_thumbnails drops the cached thumbnail for each selected image
and generates a new thumbnail.
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
None
USAGE
* enable the script in script_manager
* assign a shortcut, if desired, to apply the script by hovering
over a skull and using the shortcut to regenerate the thumbnail
BUGS, COMMENTS, SUGGESTIONS
Bill Ferguson <wpferguson@gmail.com>
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"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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 = _("regenerate thumbnails"), -- visible name of script
purpose = _("regenerate mipmap cache for selected images"), -- purpose of script
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/regenerate_thumbnails" -- URL to help/documentation
}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
local MODULE <const> = "regenerate_thumbnails"
local DEFAULT_LOG_LEVEL <const> = log.info
local TMP_DIR <const> = dt.configuration.tmp_dir
-- path separator
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
-- command separator
local CS <const> = dt.configuration.running_os == "windows" and "&" or ";"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- L O G L E V E L
-- - - - - - - - - - - - - - - - - - - - - - - -
log.log_level(DEFAULT_LOG_LEVEL)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- N A M E S P A C E
-- - - - - - - - - - - - - - - - - - - - - - - -
local regenerate_thumbnails = {}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- G L O B A L V A R I A B L E S
-- - - - - - - - - - - - - - - - - - - - - - - -
-- - - - - - - - - - - - - - - - - - - - - - - -
-- P R E F E R E N C E S
-- - - - - - - - - - - - - - - - - - - - - - - -
-- - - - - - - - - - - - - - - - - - - - - - - -
-- A L I A S E S
-- - - - - - - - - - - - - - - - - - - - - - - -
local namespace = regenerate_thumbnails
local rt = regenerate_thumbnails
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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)
log.msg(log.debug, "read value " .. tostring(val))
restore_log_level(old_log_level)
return val
end
local function pref_write(name, pref_type, value)
local old_log_level = set_log_level(sm.log_level)
log.msg(log.debug, "writing value " .. tostring(value) .. " for name " .. name)
dt.preferences.write(MODULE, name, pref_type, value)
restore_log_level(old_log_level)
end
local function stop_job()
rt.job.valid = false
end
local function generate_thumbnails(images)
local has_job = false
if #images > 50 then
rt.job = dt.gui.create_job(_("regenerating thumbnails"), true, stop_job)
has_job = true
end
for count, image in ipairs(images) do
image:drop_cache()
image:generate_cache(true, 1, 3)
if has_job then
if not rt.job.valid then
return
end
if count % 10 == 0 then
rt.job.percent = count / #images
end
end
end
if has_job then
rt.job.valid = false
end
end
local function update_button_sensitivity()
if #dt.gui.action_images > 0 then
dt.gui.libs.image.set_sensitive(MODULE, true)
else
dt.gui.libs.image.set_sensitive(MODULE, false)
end
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N P R O G R A M
-- - - - - - - - - - - - - - - - - - - - - - - -
-- - - - - - - - - - - - - - - - - - - - - - - -
-- U S E R I N T E R F A C E
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.gui.libs.image.register_action(
MODULE,
_("generate thumbnails"),
function(event, images)
generate_thumbnails(images)
end,
_("generate thumbnails")
)
update_button_sensitivity()
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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, "selection-changed")
dt.destroy_event(MODULE, "mouse-over-image-changed")
dt.gui.libs.image.destroy_action(MODULE, "regenerate thumbnails")
end
script_data.destroy = destroy
-- - - - - - - - - - - - - - - - - - - - - - - -
-- E V E N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.register_event(MODULE, "shortcut",
function(event, shortcut)
local images = dt.gui.action_images
if images then
generate_thumbnails(images)
end
end, _("regenerate thumbnails")
)
dt.register_event(MODULE, "selection-changed",
function(event)
update_button_sensitivity()
end
)
dt.register_event(MODULE, "mouse-over-image-changed",
function(event, image)
if #dt.gui.selection() < 1 then
if image then
dt.gui.libs.image.set_sensitive(MODULE, true)
else
dt.gui.libs.image.set_sensitive(MODULE, false)
end
end
end
)
return script_data

View File

@@ -38,23 +38,10 @@ 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
@@ -74,16 +61,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, string.format(_("save to buffer %d"), i))
end, "save to buffer " .. i)
dt.register_event("save_selection restore " .. i, "shortcut", function()
dt.gui.selection(saved_selection)
end, string.format(_("restore from buffer %d"), i))
end, "restore from buffer " .. 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

View File

@@ -1,225 +0,0 @@
--[[
scheduler.lua - provide scheduler services for multitasking
Copyright (C) 2024 Bill Ferguson <wpferguson@gmail.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
scheduler - provide scheduler services for multitasking
scheduler implements a sinple first in first out (FIFO) scheduler
necessary for lua script multitasking.
Scripts voluntarily yield to the scheduler and they are placed at
the end of the run queue. The first item in the run queue is told
to resume.
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
None
USAGE
start script from script_manager
BUGS, COMMENTS, SUGGESTIONS
Bill Ferguson <wpferguson@gmail.com>
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 <const> = "scheduler"
local DEFAULT_LOG_LEVEL <const> = log.info
local TMP_DIR <const> = dt.configuration.tmp_dir
local BROADCAST_MSG <const> = "broadcast"
-- path separator
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
-- command separator
local CS <const> = dt.configuration.running_os == "windows" and "&" or ";"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- A P I C H E C K
-- - - - - - - - - - - - - - - - - - - - - - - -
du.check_min_api_version("9.4.0", MODULE) -- choose the minimum version that contains the features you need
-- - - - - - - - - - - - - - - - - - - - - - - - - -
-- I 1 8 N
-- - - - - - - - - - - - - - - - - - - - - - - - - -
local gettext = dt.gettext.gettext
dt.gettext.bindtextdomain(MODULE , dt.configuration.config_dir .. "/lua/locale/")
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 = _("scheduler"), -- name of script
purpose = _("provide scheduler services for multitasking"), -- purpose of script
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/scheduler/" -- 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 scheduler = {}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- G L O B A L V A R I A B L E S
-- - - - - - - - - - - - - - - - - - - - - - - -
scheduler.run_queue = {}
scheduler.message_queue = {}
scheduler.log_level = log.debug
-- - - - - - - - - - - - - - - - - - - - - - - -
-- P R E F E R E N C E S
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.preferences.register(MODULE, "target_time_slice_seconds", "float",
_("scheduler preferred time slice"), _("target time slice before interrupt"), 1.0, 0.1, 1.0, 0.1)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- A L I A S E S
-- - - - - - - - - - - - - - - - - - - - - - - -
local sch = scheduler
-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
local function remove_from_run_queue()
log.log_level(sch.log_level)
if #sch.run_queue > 0 then
local module = sch.run_queue[1]
local message = sch.message_queue[1]
table.remove(sch.run_queue, 1)
table.remove(sch.message_queue, 1)
log.msg(log.info, "run queue length is " .. #sch.run_queue)
log.msg(log.debug, "sending " .. message .. " to " .. module)
dt.util.message(MODULE, module, message)
else
log.msg(log.warn, "run queue empty, nothing to resume")
end
end
local function add_to_run_queue(module, message)
log.log_level(sch.log_level)
table.insert(sch.run_queue, module)
table.insert(sch.message_queue, message)
log.msg(log.info, "run queue length is " .. #sch.run_queue)
dt.control.sleep(500) -- allow time for waiting scripts to start
remove_from_run_queue()
end
local function send_status(sender)
dt.util.message(MODULE, sender, "running, queue length is " .. #sch.run_queue)
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N P R O G R A M
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.util.message(MODULE, BROADCAST_MSG, "starting")
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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()
-- put things to destroy (events, storages, etc) here
dt.util.message(MODULE, BROADCAST_MSG, "stopping")
dt.destroy_event(MODULE, "inter-script-communication")
end
script_data.destroy = destroy
-- - - - - - - - - - - - - - - - - - - - - - - -
-- E V E N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.register_event(MODULE, "inter-script-communication",
function(event, sender,receiver, message)
log.log_level(sch.log_level)
if receiver == "scheduler" then
log.msg(log.debug, "got message from " .. sender .. " to " .. receiver .. " with message " .. message)
if string.match(message, "^yield") then
log.msg(log.debug, "got message " .. message .. " from " .. sender)
add_to_run_queue(sender, string.gsub(message, "yield", "resume"))
elseif message == "status" then
send_status(sender)
elseif messages == "done" then
-- do nothing
else
log.msg(log.warn, "unrecognized message " .. message)
end
end
end
)
return script_data

View File

@@ -1,266 +0,0 @@
--[[
select_duplicates.lua - select duplicate images from a collection
Copyright (C) 2026 Bill Ferguson <wpferguson@gmail.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
select_duplicates - select duplicate images from a collection
select_duplicates adds a button to the select module to select
duplicate images from a collection. If the selection2collection
script is also active an option is available to have the selected
duplicates appear as a temporary collection.
A shortcut is available to select the duplicates from the collection
but it is somewhat slower because it doesn't have access to the current
image cache and therfore has to open each image separately.
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
None
USAGE
* enable in script manager
* Press the button
BUGS, COMMENTS, SUGGESTIONS
Bill Ferguson <wpferguson@gmail.com>
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"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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 = _("select duplicates"), -- visible name of script
purpose = _("select duplicate images from a collection"), -- purpose of script
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/select_duplicates/" -- URL to help/documentation
}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
local MODULE <const> = "select_duplicates"
local DEFAULT_LOG_LEVEL <const> = log.info
local TMP_DIR <const> = dt.configuration.tmp_dir
-- path separator
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
-- command separator
local CS <const> = dt.configuration.running_os == "windows" and "&" or ";"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- L O G L E V E L
-- - - - - - - - - - - - - - - - - - - - - - - -
log.log_level(DEFAULT_LOG_LEVEL)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- N A M E S P A C E
-- - - - - - - - - - - - - - - - - - - - - - - -
local select_duplicates = {}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- G L O B A L V A R I A B L E S
-- - - - - - - - - - - - - - - - - - - - - - - -
select_duplicates.log_level = DEFAULT_LOG_LEVEL
select_duplicates.create_collection = false
-- - - - - - - - - - - - - - - - - - - - - - - -
-- P R E F E R E N C E S
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.preferences.register(MODULE, "create_collection", "bool",
script_data.metadata.name .. _(" create collection from selection"),
_("create collection of selected duplicates"), false)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- A L I A S E S
-- - - - - - - - - - - - - - - - - - - - - - - -
local namespace = select_duplicates
local sd = select_duplicates
-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
-------------------
-- helper functions
-------------------
local function set_log_level(level)
if not level then
level = namespace.log_level
end
local old_log_level = log.log_level()
log.log_level(level)
return old_log_level
end
local function restore_log_level(level)
if not level then
level = namespace.log_level
end
log.log_level(level)
end
local function reset_log_level()
log.log_level(DEFAULT_LOG_LEVEL)
namespace.log_level = DEFAULT_LOG_LEVEL
end
local function pref_read(name, pref_type)
local old_log_level = set_log_level(namespace.log_level)
log.msg(log.debug, "name is " .. name .. " and type is " .. pref_type)
local val = dt.preferences.read(MODULE, name, pref_type)
log.msg(log.debug, "read value " .. tostring(val))
restore_log_level(old_log_level)
return val
end
local function pref_write(name, pref_type, value)
local old_log_level = set_log_level(namespace.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
local function select_duplicate_files(event, images)
local duplicates = {}
local sorted_files = {}
sd.create_collection = pref_read("create_collection", "bool")
for _, img in ipairs(images) do
local rawname = img.path .. PS .. img.filename
if not sorted_files[rawname] then
sorted_files[rawname] = {}
end
table.insert(sorted_files[rawname], img)
end
for k,v in pairs(sorted_files) do
if #v > 1 then
local old_image = nil
for _,i in ipairs(v) do
if not old_image then
old_image = i
else
if i.duplicate_index > old_image.duplicate_index then
table.insert(duplicates, i)
else
table.insert(duplicates, old_image)
old_image = i
end
end
end
end
end
if sd.create_collection then
dt.util.message(MODULE, "selection2collection", "create")
end
return duplicates
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N P R O G R A M
-- - - - - - - - - - - - - - - - - - - - - - - -
-- - - - - - - - - - - - - - - - - - - - - - - -
-- U S E R I N T E R F A C E
-- - - - - - - - - - - - - - - - - - - - - - - -
dt.gui.libs.select.register_selection(MODULE, _("select duplicates"), select_duplicate_files,
_("select duplicated images"))
-- - - - - - - - - - - - - - - - - - - - - - - -
-- 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.select.destroy_selection(MODULE)
end
script_data.destroy = destroy
-- - - - - - - - - - - - - - - - - - - - - - - -
-- E V E N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
if not dt.query_event(MODULE, "shortcut") then -- skip if shortcut already registered
dt.register_event(MODULE, "shortcut",
function(event, shortcut)
local duplicates = select_duplicate_files(event, dt.collection)
if duplicates then
dt.gui.selection(duplicates)
end
end, _("select duplicates")
)
end
return script_data

Some files were not shown because too many files have changed in this diff Show More