diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 00000000..33b27bd2 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,184 @@ +## 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 + +**06 Mar 2023 - wpferguson - Added option to disable script_manager update check** + +**12 Jan 2022 - wpferguson - Documented contrib/rename_images.lua** + +**28 Dec 2021 - wpferguson - Replaced deprecated _which_ command with _command -v_ in lib/dtutils/file.lua** + +**12 Dec 2021 - wpferguson** +* Added deprecated function to library +* Added deprecation warning to contrib/rename-tags.lua + +**22 Oct 2021 - wpferguson for Volker Bödker - make sure sequence is 4 digits in rename_images.lua** + +**31 Aug 2021 - wpferguson - remove styles hiding from AutoGrouper.lua** + +**02 Jul 2021 - wpferguson - merged API-7.0.0-dev branch to master** +* API-7.0.0 is darktable 3.6 +* breaking changes + * register_event argments changed + * register_action arguments changed + * register_selection arguments changed + * register_event arguments changed +* scripts updated to API-7.0.0 compatibility +* script_manger updated to be API aware and check out the proper branch + based on darktable API version + +**01 Jul 2021 - wpferguson - created branch for API-6.1.0 - darktable 3.4** + +**20 Jun 2021 - wpferguson - created branches for older API versions** +* API-6.0.0 - darktable 3.2.1 +* API-5.0.2 - darktable 3.0 +* API-5.0.1 - darktable 2.6.1 +* API-5.0.0 - darktable 2.4 +* API-4.0.0 - darktable 2.2 +* API-3.0.0 - darktable 2.0 + +**19 Jun 2021 - wpferguson - fix issue 312, image_path_in_ui** + +**02 Jun 2021 - wpferguson - fix contrib/quicktag** +* set new entry field is_password to false so entry +is visible to user while typing. + +**19 Mar 2021 - wpferguson - fixed crash in contrib/HDRmerge.lua** +* Made generated filename routine gracefully handle names that +are not in the expected format. + +**15 Mar 2021 - scheckmedia - updated contrib/photils.lua** +* refactor print method +* add option to apply selected tags from a single image to multiple images +* add setting parameter to enable/disable the export of an image before tag suggestion + +**25 Feb 2021 - wpferguson - added detached mode to contrib/gimp.lua** + +* Added run_detached checkbox to the exporter GUI. Selecting run_detached +let's GIMP keep running and accepting additional images. It does not return +the edited images to darktable. + +**24 Feb 2021 - Mark64 - make ext_editor lib visible in darkroom view** + +**17 Feb 2021 - wpferguson - API 6.2.3 register_action changes** + +* Added check for API version and supplied a name argument if the +API version was greater than or equal to 6.2.3 + +**10 Feb 2021 - wpferguson - bugfix select_untagged** + +* Fixed callback to return a list of images as expected instead of +doing the selection in the callback + +**10 Feb 2021 - wpferguson - bugfix API 6.2.1 compatibility** + +* The inline check for API version didn't handle argument return +correctly so added a transition library with a register_event function +override to check the API version and process the arguments correctly. + +**9 Feb 2021 - wpferguson - bugfix API 6.2.2 compatibility** + +* The inline check for API version didn't handle argument return +correctly so changed it to a full if/else block + +**4 Reb 2021 - wpferguson - API 6.2.2 compatibililty** + +* Added check for API version and supplied a name argument to register_selection +if the API version was greater than or eqal to 6.2.2 + +**1 Feb 2021 - wpferguson - API 6.2.1 compatibility** + +* Added check for API version and supplied a name argument to register_event +if the API version was greater than or eqal to 6.2.1 + +**21 Jan 2021 - wpferguson - Modified dtutils function find_image_by_id** + +* For users with API 6.2.0 or greater - Enabled use of new API function +darktable.database_get_image() in find_image_by_id(). + +**19 Jan 2021 - schwerdf - Added dtutils library function find_image_by_id()** + +* Added new library function to retrieve an image from the library based on it's ID instead +of it's row number in the database + +**10 Jan 2021 - chrisaga - copy_attach_detach_tags localization** + +**7 Jan 2021 - dtorop - add contrib/fujifilm_dynamic_range** + +* add a new contrib script, fujifilm_dynamic_range to adjust exposure +based on the exposure bias camera setting diff --git a/README.md b/README.md index 9968ac65..8caa5c2e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,276 @@ -lua-scripts -=========== +# Lua scripts -The Lua scripts in this repository are meant to be used together with darktable. Either copy them individually to `~/.config/darktable/lua` (Linux/Unix) or `%LOCALAPPDATA%\darktable\lua` (Windows) (you might have to create that folder) or just copy/symlink the whole repository there. That allows to update all your scripts with a simple call to `git pull`. +## Description -To enable one of the scripts you have to add a line like `require "examples/hello_world"` to your: `~/.config/darktable/luarc` (Linux/Unix) or `%LOCALAPPDATA%\darktable\luarc` (Windows) file which will enable the example script in `examples/hello_world.lua` (note the lack of the `.lua` suffix). +darktable can be customized and extended using the Lua programming language. This repository contains the collected +efforts of the darktable developers, maintainers, contributors and community. The following sections list the scripts +contained in the repository, whether they can be run by themselves (Standalone - Yes) or depend on other +scripts (Standalone - No), what operating systems they are known to work on (L - Linux, M - MacOS, W - Windows), and their purpose. -Each script includes its own documentation and usage in its header, please refer to them. +For the latest changes, see the [ChangeLog](ChangeLog.md) -In order to have your own scripts added here they have to be under a free license (GPL2+ will definitely work, others can be discussed). Scripts in the `official/` subfolder are maintained by the darktable community, those under `contrib/` are meant to have an "owner" who maintains them. +### Official Scripts + +These scripts are written primarily by the darktable developers and maintained by the authors and/or repository maintainers. They are located in the official/ directory. + +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 + + +### Contributed Scripts + +These scripts are contributed by users. They are meant to have an "owner", i.e. the author, who maintains them. Over time the community has helped maintain these scripts, as well as the authors. They are located in the contrib/ directory. + +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 +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 + +### Example Scripts + +These scripts provide examples of how to use specific portions of the API. They run, but are meant for demonstration purposes only. They are located in the examples/ directory. + +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 + +### Tools + +Tool scripts perform functions relating to the repository, such as generating documentation. They are located in the tools/ directory. + +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 + +### Related third-party projects + +The following third-party projects are listed for information only. Think of this collection as an `awesome-darktable-lua-scripts` list. Use at your own risk! + +* [trougnouf/dtMediaWiki](https://github.com/trougnouf/dtMediaWiki) – Wikimedia Commons export +* [wpferguson/extra-dt-lua-scripts](https://github.com/wpferguson/extra-dt-lua-scripts) +* [xxv/darktable-git-annex](https://github.com/xxv/darktable-git-annex) – git-annex integration +* [theres/dt_backup](https://github.com/theres/dt_backup) – automatic backup on exit +* [Sitwon/dt_fujifilm_ratings](https://github.com/Sitwon/dt_fujifilm_ratings) – Fujifilm Ratings import +* [progo/leica-q-autocrop](https://github.com/progo/leica-q-autocrop) – crop in 35 mm or 50 mm equivalent +* [BzKevin/OpenInPS-Darktable-PlugIn](https://github.com/BzKevin/OpenInPS-Darktable-PlugIn) – open in Adobe Photoshop +* [johnnyrun/darktable_lua_gimp](https://github.com/johnnyrun/darktable_lua_gimp) – GIMP export +* [arru/darktable-scripts](https://github.com/arru/darktable-scripts) +* [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 + +The recommended method of installation is using git to clone the repository. This ensures that all dependencies on other scripts +are met as well as providing an easy update path. Single scripts listed as standalone may be downloaded and installed by themselves. + +### snap packages + +The snap version of darktable comes with lua included starting with version 2.4.3snap2. + +Ensure git is installed on your system. If it isn't, use the package manager to install it. Then open a terminal and: + + cd ~/snap/darktable/current + git clone https://github.com/darktable-org/lua-scripts.git lua + +### flatpak packages + +Flatpak packages now use the internal lua interpreter. + + +Ensure git is installed on your system. If it isn't, use the package manager to install it. Then open a terminal and: + + cd ~/.var/app/org.darktable.Darktable/config/darktable + git clone https://github.com/darktable-org/lua-scripts.git lua + +### appimage packages + +These packages run in their own environment and don't have access to a lua interpreter, therefore the scripts can't run. The packagers could enable the internal interpreter, or allow the package to link the interpreter from the operating system, or bundle a copy of lua with the package. If you use one of these packages and wish to use the lua scripts, please contact the package maintainer and suggest the above fixes. + +### Linux and MacOS + +Ensure git is installed on your system. If it isn't, use the package manager to install it. Then open a terminal and: + + cd ~/.config/darktable/ + git clone https://github.com/darktable-org/lua-scripts.git lua + +### Windows + +Ensure git is installed on your system. Git can be obtained from https://gitforwindows.org/, as well as other places. If you use the gitforwindows.org distribution, install the Git Bash Shell also as it will aid in debugging the scripts if necessary. Then open a command prompt and run: + + cd %LOCALAPPDATA%\darktable + git clone https://github.com/darktable-org/lua-scripts.git lua + +If you don't have %LOCALAPPDATA%\darktable you have to start dartable at least once, because the directory is created at the first start of darktable. + +## Enabling + +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 "/"` 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 module. To use script manager do the following: + +### Linux or MacOS + + echo 'require "tools/script_manager"' > ~/.config/darktable/luarc + +### Windows + + echo require "tools/script_manager" > %LOCALAPPDATA%\darktable\luarc + +### Snap + + echo 'require "tools/script_manager"' > ~/snap/darktable/current/luarc + +### Flatpak + + echo 'require "tools/script_manager"' > ~/.var/app/org.darktable.Darktable/config/darktable/luarc + +You can also create or add lines to the luarc file from the command line: + +`echo 'require "contrib/gimp"' > ~/.config/darktable/luarc` to create the file with a gimp entry\ +or `echo 'require "contrib/hugin"' >> ~/.config/darktable/luarc` to add an entry for hugin. + +On windows from a command prompt: + +`echo require "contrib/gimp" > %LOCALAPPDATA%\darktable\luarc` to create the file with a gimp entry\ +or `echo require "contrib/hugin" >> %LOCALAPPDATA%\darktable\luarc` to add an entry for hugin. + +## Disabling + +To disable a script open the luarc file in your text editor and insert `--` at the start of the line containing the script you wish to disable, then save the file. + +## Updating + +To update the script repository, open a terminal or command prompt and do the following: + +### Snap + + cd ~/snap/darktable/current/lua + git pull + + +### Flatpak + + cd ~/.var/app/org.darktable.Darktable/config/darktable/lua + git pull + +### Linux and MacOS + + cd ~/.config/darktable/lua/ + git pull + +### Windows + + cd %LOCALAPPDATA%\darktable\lua + git pull + +## Documentation + +The [Lua Scripts Manual](https://docs.darktable.org/lua/stable/lua.scripts.manual/) provides documentation +for the scripts transcribed from the header comments. Each script also contains comments and usage instructions +in the header comments. + +The [Lua Scripts Library API Manual](https://docs.darktable.org/lua/stable/lua.scripts.api.manual/) provides +documentation of the libraries and functions. Lua-script libraries documentation may also be generated using +the tools in the tools/ directory. + +More information about the scripting with Lua can be found in the darktable user manual: +[Scripting with Lua](https://darktable.org.github.io/dtdocs/lua/) + +The [Lua API Manual](https://docs.darktable.org/lua/stable/lua.api.manual/) provides docuemntation of the +darktable Lua API. + +## Troubleshooting + +Running darktable with Lua debugging enabled provides more information about what is occurring within the scripts. + +### Snap + +Open a terminal and start darktable with the command `snap run darktable -d lua`. This provides debugging information to give you insight into what is happening. + +### Linux + +Open a terminal and start darktable with the command `darktable -d lua`. This provides debugging information to give you insight into what is happening. + +### MacOS + +Open a terminal and start darktable with the command `/Applications/darktable.app/Contents/MacOS/darktable -d lua`. This provides debugging information to give you insight into what is happening. + +### Windows + +Open a command prompt. Start darktable with the command "C:\Program Files\darktable\bin\darktable" -d lua > log.txt. This provides debugging information to give you insight into what is happening. + +## Contributing + +In order to have your own scripts added here they have to be under a free license (GPL2+ will definitely work, others can be discussed). diff --git a/contrib/AutoGrouper.lua b/contrib/AutoGrouper.lua new file mode 100644 index 00000000..889c9af1 --- /dev/null +++ b/contrib/AutoGrouper.lua @@ -0,0 +1,225 @@ +--[[AutoGrouper plugin for darktable + + copyright (c) 2019 Kevin Ertel + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +]] + +--[[About this Plugin +This plugin adds the module "Auto Group" to darktable's lighttable view + +----REQUIRED SOFTWARE---- +None + +----USAGE---- +Install: (see here for more detail: https://github.com/darktable-org/lua-scripts ) + 1) Copy this file in to your "lua/contrib" folder where all other scripts reside. + 2) Require this file in your luarc file, as with any other dt plug-in + +Set a gap amount in second which will be used to determine when images should no +longer be added to a group. If an image is more then the specified amount of time +from the last image in the group it will not be added. Images without timestamps +in exif data will be ignored. + +There are two buttons. One allows the grouping to be performed only on the currently +selected images, the other button performs grouping on the entire active collection +]] + +local dt = require "darktable" +local du = require "lib/dtutils" + +du.check_min_api_version("7.0.0", "AutoGrouper") + +local MOD = 'autogrouper' + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("auto group"), + purpose = _("automatically group images by time interval"), + author = "Kevin Ertel", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/AutoGrouper/" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +local Ag = {} +Ag.module_installed = false +Ag.event_registered = false + +local GUI = { + gap = {}, + selected = {}, + collection = {} +} + + +local function InRange(test, low, high) --tests if test value is within range of low and high (inclusive) + if test >= low and test <= high then + return true + else + return false + end +end + +local function CompTime(first, second) --compares the timestamps and returns true if first was taken before second + first_time = first.exif_datetime_taken + if string.match(first_time, '[0-9]') == nil then first_time = '9999:99:99 99:99:99' end + first_time = tonumber(string.gsub(first_time, '[^0-9]*','')) + second_time = second.exif_datetime_taken + if string.match(second_time, '[0-9]') == nil then second_time = '9999:99:99 99:99:99' end + second_time = tonumber(string.gsub(second_time, '[^0-9]*','')) + return first_time < second_time +end + +local function SeperateTime(str) --seperates the timestamp into individual components for used with OS.time operations + local cleaned = string.gsub(str, '[^%d]',':') + cleaned = string.gsub(cleaned, '::*',':') --YYYY:MM:DD:hh:mm:ss + local year = string.sub(cleaned,1,4) + local month = string.sub(cleaned,6,7) + local day = string.sub(cleaned,9,10) + local hour = string.sub(cleaned,12,13) + local min = string.sub(cleaned,15,16) + local sec = string.sub(cleaned,18,19) + return {year = year, month = month, day = day, hour = hour, min = min, sec = sec} +end + +local function GetTimeDiff(curr_image, prev_image) --returns the time difference (in sec.) from current image and the previous image + local curr_time = SeperateTime(curr_image.exif_datetime_taken) + local prev_time = SeperateTime(prev_image.exif_datetime_taken) + return os.time(curr_time)-os.time(prev_time) +end + +local function main(on_collection) + local images = {} + if on_collection then + local col_images = dt.collection + for i,image in ipairs(col_images) do --copy images to a standard table, table.sort barfs on type dt_lua_singleton_image_collection + table.insert(images,i,image) + end + else + images = dt.gui.selection() + end + dt.preferences.write(MOD, 'active_gap', 'integer', GUI.gap.value) + if #images < 2 then + dt.print('please select at least 2 images') + return + end + table.sort(images, function(first, second) return CompTime(first,second) end) --sort images by timestamp + + for i, image in ipairs(images) do + if i == 1 then + prev_image = image + image:make_group_leader() + elseif string.match(image.exif_datetime_taken, '[%d]') ~= nil then --make sure current image has a timestamp, if so check if it is within the user specified gap value and add to group + local curr_image = image + if GetTimeDiff(curr_image, prev_image) <= GUI.gap.value then + images[i]:group_with(images[i-1].group_leader) + end + prev_image = curr_image + end + end +end + +local function install_module() + if not Ag.module_installed then + dt.print_log("installing module") + dt.register_lib( + MOD, -- Module name + _('auto group'), -- name + true, -- expandable + true, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 99}}, -- containers + dt.new_widget("box"){ + orientation = "vertical", + GUI.gap, + GUI.selected, + GUI.collection + } + ) + Ag.module_installed = true + dt.print_log("module installed") + dt.print_log("styles module visibility is " .. tostring(dt.gui.libs["styles"].visible)) + end +end + +local function destroy() + dt.gui.libs[MOD].visible = false +end + +local function restart() + dt.gui.libs[MOD].visible = true +end + +-- GUI -- +temp = dt.preferences.read(MOD, 'active_gap', 'integer') +if not InRange(temp, 1, 86400) then temp = 3 end +GUI.gap = dt.new_widget('slider'){ + label = _('group gap [sec.]'), + tooltip = _('minimum gap, in seconds, between groups'), + soft_min = 1, + soft_max = 60, + hard_min = 1, + hard_max = 86400, + step = 1, + digits = 0, + value = temp, + reset_callback = function(self) + self.value = 3 + end +} +GUI.selected = dt.new_widget("button"){ + label = _('auto group: selected'), + tooltip =_('auto group selected images'), + clicked_callback = function() main(false) end +} +GUI.collection = dt.new_widget("button"){ + label = _('auto group: collection'), + tooltip =_('auto group the entire collection'), + clicked_callback = function() main(true) end +} + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not Ag.event_registered then + dt.register_event( + "AutoGrouper", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + Ag.event_registered = true + end +end + +script_data.destroy = destroy +script_data.destroy_method = "hide" +script_data.restart = restart +script_data.show = restart + +return script_data diff --git a/contrib/CollectHelper.lua b/contrib/CollectHelper.lua new file mode 100644 index 00000000..978aa309 --- /dev/null +++ b/contrib/CollectHelper.lua @@ -0,0 +1,258 @@ +--[[Collect Helper plugin for darktable + + copyright (c) 2019 Kevin Ertel + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +]] + +--[[About this plugin +This plugin adds the button(s) to the "Selected Images" module: +1) Return to Previous Collection +2) Collect on image's Folder +3) Collect on image's Color Label(s) +4) Collect on All (AND) + +It also adds 3 preferences to the lua options dialog box which allow the user to activate/deactivate the 3 "Collect on" buttons. + +Button Behavior: +1) Return to Previous Collection - Will reset the collect parameters to the previously active settings +2) Collect on image's Folder - Will change the collect parameters to be "Folder" with a value of the selected image's folder location +3) Collect on image's Color Label(s) - Will change the collect parameter to be "Color" with a value of the selected images color labels, will apply multiple parameters with AND logic if multiple exist +4) Collect on All (AND) - Will collect on all parameters activated by the preferences dialog, as such this button is redundant if you only have one of the two other options enabled + +----REQUIRED SOFTWARE---- +NA + +----USAGE---- +Install: (see here for more detail: https://github.com/darktable-org/lua-scripts ) + 1) Copy this file in to your "lua/contrib" folder where all other scripts reside. + 2) Require this file in your luarc file, as with any other dt plug-in + +Select the photo you wish to change you collection based on. +In the "Selected Images" module click on "Collect on this Image" + +----KNOWN ISSUES---- +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local gettext = dt.gettext.gettext +local previous = nil +local all_active = false + +du.check_min_api_version("7.0.0", "CollectHelper") + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("collection helper"), + purpose = _("add collection helper buttons"), + author = "Kevin Ertel", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/CollectHelper/" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +-- FUNCTION -- +local function CheckSingleImage(selection) + if #selection ~= 1 then + dt.print(_("please select a single image")) + return true + end + return false +end + +local function CheckHasColorLabel(selection) + local ret = false + for _,image in pairs(selection) do + if image.red then ret = true end + if image.blue then ret = true end + if image.green then ret = true end + if image.yellow then ret = true end + if image.purple then ret = true end + end + return ret +end +local function PreviousCollection() + if previous ~= nil then + previous = dt.gui.libs.collect.filter(previous) + end +end + +local function CollectOnFolder(all_rules, all_active) + local images = dt.gui.selection() + if CheckSingleImage(images) then + return + end + local rules = {} + local rule = dt.gui.libs.collect.new_rule() + rule.mode = "DT_LIB_COLLECT_MODE_AND" + rule.data = images[1].path + rule.item = "DT_COLLECTION_PROP_FOLDERS" + table.insert(rules, rule) + if all_active then + for _,active_rule in pairs(rules) do + table.insert(all_rules, active_rule) + end + return all_rules + else + previous = dt.gui.libs.collect.filter(rules) + end +end + +local function CollectOnColors(all_rules, all_active) + local images = dt.gui.selection() + if CheckSingleImage(images) then + return + end + if not CheckHasColorLabel(images) then + dt.print(_('select an image with an active color label')) + return + end + for _,image in pairs(images) do + local rules = {} + if image.red then + local red_rule = dt.gui.libs.collect.new_rule() + red_rule.mode = "DT_LIB_COLLECT_MODE_AND" + red_rule.data = "red" + red_rule.item = "DT_COLLECTION_PROP_COLORLABEL" + table.insert(rules, red_rule) + end + if image.blue then + local blue_rule = dt.gui.libs.collect.new_rule() + blue_rule.mode = "DT_LIB_COLLECT_MODE_AND" + blue_rule.data = "blue" + blue_rule.item = "DT_COLLECTION_PROP_COLORLABEL" + table.insert(rules, blue_rule) + end + if image.green then + local green_rule = dt.gui.libs.collect.new_rule() + green_rule.mode = "DT_LIB_COLLECT_MODE_AND" + green_rule.data = "green" + green_rule.item = "DT_COLLECTION_PROP_COLORLABEL" + table.insert(rules, green_rule) + end + if image.yellow then + local yellow_rule = dt.gui.libs.collect.new_rule() + yellow_rule.mode = "DT_LIB_COLLECT_MODE_AND" + yellow_rule.data = "yellow" + yellow_rule.item = "DT_COLLECTION_PROP_COLORLABEL" + table.insert(rules, yellow_rule) + end + if image.purple then + local purple_rule = dt.gui.libs.collect.new_rule() + purple_rule.mode = "DT_LIB_COLLECT_MODE_AND" + purple_rule.data = "purple" + purple_rule.item = "DT_COLLECTION_PROP_COLORLABEL" + table.insert(rules, purple_rule) + end + if all_active then + for _,active_rule in pairs(rules) do + table.insert(all_rules, active_rule) + end + return all_rules + else + previous = dt.gui.libs.collect.filter(rules) + end + end +end + +local function CollectOnAll_AND() + local images = dt.gui.selection() + if CheckSingleImage(images) then + return + end + local rules = {} + if dt.preferences.read('module_CollectHelper','folder','bool') then + rules = CollectOnFolder(rules, true) + end + if dt.preferences.read('module_CollectHelper','colors','bool') then + rules = CollectOnColors(rules, true) + end + previous = dt.gui.libs.collect.filter(rules) +end + +local function destroy() + dt.gui.libs.image.destroy_action("CollectHelper_prev") + if dt.preferences.read('module_CollectHelper','folder','bool') then + dt.gui.libs.image.destroy_action("CollectHelper_folder") + end + if dt.preferences.read('module_CollectHelper','colors','bool') then + dt.gui.libs.image.destroy_action("CollectHelper_labels") + end + if dt.preferences.read('module_CollectHelper','all_and','bool') then + dt.gui.libs.image.destroy_action("CollectHelper_and") + end +end + +-- GUI -- +dt.gui.libs.image.register_action( + "CollectHelper_prev", _("collect: previous"), + function() PreviousCollection() end, + _("sets the collect parameters to be the previously active parameters") +) +if dt.preferences.read('module_CollectHelper','folder','bool') then + dt.gui.libs.image.register_action( + "CollectHelper_folder", _("collect: folder"), + function() CollectOnFolder(_ , false) end, + _("sets the collect parameters to be the selected images's folder") + ) +end +if dt.preferences.read('module_CollectHelper','colors','bool') then + dt.gui.libs.image.register_action( + "CollectHelper_labels", _("collect: color label(s)"), + function() CollectOnColors(_ , false) end, + _("sets the collect parameters to be the selected images's color label(s)") + ) +end +if dt.preferences.read('module_CollectHelper','all_and','bool') then + dt.gui.libs.image.register_action( + "CollectHelper_and", _("collect: all (AND)"), + function() CollectOnAll_AND() end, + _("sets the collect parameters based on all activated CollectHelper options") + ) +end + +-- PREFERENCES -- +dt.preferences.register("module_CollectHelper", "all_and", -- name + "bool", -- type + _('CollectHelper: all'), -- label + _('creates a collect parameter set that utilizes all enabled CollectHelper types (and)'), -- tooltip + true -- default +) +dt.preferences.register("module_CollectHelper", "colors", -- name + "bool", -- type + _('CollectHelper: color label(s)'), -- label + _('enable the button that allows you to swap to a collection based on selected image\'s color label(s)'), -- tooltip + true -- default +) +dt.preferences.register("module_CollectHelper", "folder", -- name + "bool", -- type + _('CollectHelper: folder'), -- label + _('enable the button that allows you to swap to a collection based on selected image\'s folder location'), -- tooltip + true -- default +) + +script_data.destroy = destroy + +return script_data diff --git a/contrib/HDRMerge.lua b/contrib/HDRMerge.lua new file mode 100644 index 00000000..9d7a951d --- /dev/null +++ b/contrib/HDRMerge.lua @@ -0,0 +1,486 @@ +--[[HDRMerge plugin for darktable + + copyright (c) 2018 Kevin Ertel + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +]] + +--[[About this Plugin +This plugin adds the module 'HDRMerge' to darktable's lighttable view + +----REQUIRED SOFTWARE---- +HDRMerge ver. 4.5 or greater + +----USAGE---- +Install: (see here for more detail: https://github.com/darktable-org/lua-scripts ) + 1) Copy this file in to your 'lua/contrib' folder where all other scripts reside. + 2) Require this file in your luarc file, as with any other dt plug-in +On the initial startup go to darktable settings > lua options and set your executable paths and other preferences, then restart darktable + +Select bracketed images and press the Run HDRMerge button. The resulting DNG will be auto-imported into darktable. +Additional tags or style can be applied on auto import as well, if you desire. + +Base Options: +Select your desired BPS (bits per sample and Embedded Preview Size. + +Batch Options: +Select if you want to run in batch mode or not +Select the gap, in seconds, between images for auto grouping in batch mode + +See HDRMerge manual for further detail: http://jcelaya.github.io/hdrmerge/documentation/2014/07/11/user-manual.html + +Auto-import Options: +Select a style, whether you want tags to be copied from the original, and any additional tags you desire added when the new image is auto-imported +]] + +local dt = require 'darktable' +local du = require "lib/dtutils" +local df = require 'lib/dtutils.file' +local dsys = require 'lib/dtutils.system' + +du.check_min_api_version("7.0.0", "HDRmerge") + +-- Tell gettext where to find the .mo file translating messages for a particular domain +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("HDR merge"), + purpose = _("merge bracketed images into an HDR DNG image"), + author = "Kevin Ertel", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/HDRmerge" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + + +local mod = 'module_HDRMerge' +local os_path_seperator = '/' +if dt.configuration.running_os == 'windows' then os_path_seperator = '\\' end +local CURR_API_STRING = dt.configuration.api_version_string + +local temp +local HDRM = { --HDRMerge Program Table + name = 'HDRMerge', + bin = '', + first_run = true, + install_error = false, + arg_string = '', + images_string = '', + args = { + bps = {text = '-b ', style = 'integer'}, + size = {text = '-p ', style = 'string'}, + batch = {text = '-B ', style = 'bool'}, + gap = {text = '-g ', style = 'integer'} + } +} +local GUI = { --GUI Elements Table + HDR = { + bps ={}, + size ={}, + batch ={}, + gap ={} + }, + Target = { + style ={}, + copy_tags ={}, + add_tags ={} + }, + run = {}, + stack = {}, + options = {}, + exes = { + HDRMerge = {}, + update = {}, + } +} + +HDRM.module_installed = false +HDRM.event_registered = false + + +--Detect User Styles-- +local styles = dt.styles +local styles_count = 1 -- 'none' = 1 +for _,i in pairs(dt.styles) do + if type(i) == 'userdata' then styles_count = styles_count + 1 end +end + +local function InRange(test, low, high) --tests if test value is within range of low and high (inclusive) + if test >= low and test <= high then + return true + else + return false + end +end + +local function GetFileName(full_path) --Parses a full path (path/filename_identifier.extension) into individual parts +--[[Input: Folder1/Folder2/Folder3/Img_0001.CR2 + + Returns: + path: Folder1/Folder2/Folder3/ + filename: Img_0001 + identifier: 0001 + extension: .CR2 + + EX: + path_1, file_1, id_1, ext_1 = GetFileName(full_path_1) + ]] + local path = string.match(full_path, '.*[\\/]') + local filename = string.gsub(string.match(full_path, '[%w-_]*%.') , '%.' , '' ) + local identifier = string.match(filename, '%d*$') + local extension = string.match(full_path, '%.%w*') + return path, filename, identifier, extension +end + +local function CleanSpaces(text) --removes spaces from the front and back of passed in text + text = string.gsub(text,'^%s*','') + text = string.gsub(text,'%s*$','') + return text +end + +local function BuildExecuteCmd(prog_table) --creates a program command using elements of the passed in program table + local result = CleanSpaces(prog_table.bin)..' '..CleanSpaces(prog_table.arg_string)..' '..CleanSpaces(prog_table.images_string) + return result +end + +local function PreCall(prog_tbl) --looks to see if this is the first call, if so checks to see if program is installed properly + for _,prog in pairs(prog_tbl) do + if prog.first_run then + prog.bin = df.check_if_bin_exists(prog.name) + if not prog.bin then + prog.install_error = true + dt.preferences.write(mod, 'bin_exists', 'bool', false) + else + prog.bin = CleanSpaces(prog.bin) + end + prog.first_run = false + end + end + if not dt.preferences.read(mod, 'bin_exists', 'bool') then + GUI.stack.active = 2 + dt.print(_('please update you binary location')) + end +end + +local function ExeUpdate(prog_tbl) + dt.preferences.write(mod, 'bin_exists', 'bool', true) + for _,prog in pairs(prog_tbl) do + dt.preferences.write('executable_paths', prog.name, 'string', GUI.exes[prog.name].value) + prog.bin = df.check_if_bin_exists(prog.name) + if not prog.bin then + prog.install_error = true + dt.preferences.write(mod, 'bin_exists', 'bool', false) + else + prog.bin = CleanSpaces(prog.bin) + end + prog.first_run = false + end + if dt.preferences.read(mod, 'bin_exists', 'bool') then + GUI.stack.active = 1 + dt.print(_('update successful')) + else + dt.print(_('update unsuccessful, please try again')) + end +end + +local function UpdateActivePreference() --sliders & entry boxes do not have a click/changed callback, so their values must be saved to the active preference + temp = GUI.HDR.gap.value + dt.preferences.write(mod, 'active_gap', 'integer', temp) + temp = GUI.Target.add_tags.text + dt.preferences.write(mod, 'active_add_tags', 'string', temp) +end + +local function main() + PreCall({HDRM}) --check if furst run then check if install OK + if HDRM.install_error then + dt.print_error('HDRMerge install issue') + dt.print(_('HDRMerge install issue, please ensure the binary path is correct')) + return + end + images = dt.gui.selection() --get selected images + if #images < 2 then --ensure enough images selected + dt.print(_('not enough images selected, select at least 2 images to merge')) + return + end + + UpdateActivePreference() --save current gui elements to active preference so those values will be pre-loaded at next startup + + --create image string and output path + HDRM.images_string = '' + local out_path = '' + local smallest_id = math.huge + local smallest_name = '' + local largest_id = 0 + local source_raw = {} + for _,image in pairs(images) do --loop to concat the images string, also track the image indexes for use in creating the final image name (eg; IMG_1034-1037.dng) + local curr_image = image.path..os_path_seperator..image.filename + HDRM.images_string = HDRM.images_string..df.sanitize_filename(curr_image)..' ' + out_path = image.path + _unused, source_name, source_id = GetFileName(image.filename) + source_id = tonumber(source_id) or 0 + if source_id < smallest_id then + smallest_id = source_id + smallest_name = source_name + source_raw = image + end + if source_id > largest_id then largest_id = source_id end + end + out_path = out_path..os_path_seperator..smallest_name..'-'..largest_id..'.dng' + out_path = df.create_unique_filename(out_path) + + --create argument string + HDRM.arg_string = HDRM.args.bps.text..GUI.HDR.bps.value..' '..HDRM.args.size.text..GUI.HDR.size.value..' ' + if GUI.HDR.batch.value then + HDRM.arg_string = HDRM.arg_string..HDRM.args.batch.text..HDRM.args.gap.text..math.floor(GUI.HDR.gap.value)..' -a' + else + HDRM.arg_string = HDRM.arg_string..'-o '..df.sanitize_filename(out_path) + end + + -- create run command and execute + local run_cmd = BuildExecuteCmd(HDRM) + resp = dsys.external_command(run_cmd) + + if resp == 0 and not GUI.HDR.batch.value then + local imported = dt.database.import(out_path) -- import the new file + if GUI.Target.style.selected > 1 then -- apply selected style + local set_style = styles[GUI.Target.style.selected - 1] + dt.styles.apply(set_style , imported) + end + if GUI.Target.copy_tags.value then -- copy tags from the original file (ignore 'darktable' generated tags) + local all_tags = dt.tags.get_tags(source_raw) + for _,tag in pairs(all_tags) do + if string.match(tag.name, 'darktable|') == nil then dt.tags.attach(tag, imported) end + end + end + local set_tag = GUI.Target.add_tags.text + if set_tag ~= nil then -- add additional user-specified tags + for tag in string.gmatch(set_tag, '[^,]+') do + tag = CleanSpaces(tag) + tag = dt.tags.create(tag) + dt.tags.attach(tag, imported) + end + end + dt.print(_('HDRMerge completed successfully')) + else + dt.print_error('HDRMerge failed') + dt.print(_('HDRMerge failed')) + end + +end + +local function install_module() + if not HDRM.module_installed then + dt.register_lib( -- register HDRMerge module + 'HDRMerge_Lib', -- Module name + _('HDRMerge'), -- name + true, -- expandable + true, -- resetable + {[dt.gui.views.lighttable] = {'DT_UI_CONTAINER_PANEL_RIGHT_CENTER', 99}}, -- containers + dt.new_widget('box'){ + orientation = 'vertical', + GUI.stack + } + ) + HDRM.module_installed = true + end +end + +local function destroy() + dt.gui.libs["HDRMerge_Lib"].visible = false +end + +local function restart() + dt.gui.libs["HDRMerge_Lib"].visible = true +end + +-- GUI Elements -- +local lbl_hdr = dt.new_widget('section_label'){ + label = _('HDRMerge options') +} +temp = dt.preferences.read(mod, 'active_bps_ind', 'integer') +if not InRange(temp, 1, 3) then temp = 3 end +GUI.HDR.bps = dt.new_widget('combobox'){ + label = _('bits per sample'), + tooltip =_('number of bits per sample in the output image'), + selected = temp, + '16','24','32', + changed_callback = function(self) + dt.preferences.write(mod, 'active_bps', 'integer', self.value) + dt.preferences.write(mod, 'active_bps_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 3 + dt.preferences.write(mod, 'active_bps', 'integer', self.value) + dt.preferences.write(mod, 'active_bps_ind', 'integer', self.selected) + end +} +temp = dt.preferences.read(mod, 'active_size_ind', 'integer') +if not InRange(temp, 1, 3) then temp = 2 end +GUI.HDR.size = dt.new_widget('combobox'){ + label = _('embedded preview size'), + tooltip =_('size of the embedded preview in output image'), + selected = temp, + _('none'),_('half'),_('full'), + changed_callback = function(self) + dt.preferences.write(mod, 'active_size', 'string', self.value) + dt.preferences.write(mod, 'active_size_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 2 + dt.preferences.write(mod, 'active_size', 'string', self.value) + dt.preferences.write(mod, 'active_size_ind', 'integer', self.selected) + end +} +GUI.HDR.batch = dt.new_widget('check_button'){ + label = _('batch mode'), + value = dt.preferences.read(mod, 'active_batch', 'bool'), + tooltip = _('enable batch mode operation \nNOTE: resultant files will NOT be auto-imported'), + clicked_callback = function(self) + dt.preferences.write(mod, 'active_batch', 'bool', self.value) + GUI.HDR.gap.sensitive = self.value + end, + reset_callback = function(self) self.value = false end +} +temp = dt.preferences.read(mod, 'active_gap', 'integer') +if not InRange(temp, 1, 3600) then temp = 3 end +GUI.HDR.gap = dt.new_widget('slider'){ + label = _('batch gap [sec.]'), + tooltip = _('gap, in seconds, between batch mode groups'), + soft_min = 1, + soft_max = 30, + hard_min = 1, + hard_max = 3600, + step = 1, + digits = 0, + value = temp, + sensitive = GUI.HDR.batch.value, + reset_callback = function(self) + self.value = 3 + end +} +local lbl_import = dt.new_widget('section_label'){ + label = _('import options') +} +GUI.Target.style = dt.new_widget('combobox'){ + label = _('apply style on import'), + tooltip = _('apply selected style on auto-import to newly created image'), + selected = 1, + _('none'), + changed_callback = function(self) + dt.preferences.write(mod, 'active_style', 'string', self.value) + dt.preferences.write(mod, 'active_style_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 1 + dt.preferences.write(mod, 'active_style', 'string', self.value) + dt.preferences.write(mod, 'active_style_ind', 'integer', self.selected) + end +} +for k=1, (styles_count-1) do + GUI.Target.style[k+1] = styles[k].name +end +temp = dt.preferences.read(mod, 'active_style_ind', 'integer') +if not InRange(temp, 1, styles_count) then temp = 1 end +GUI.Target.style.selected = temp +GUI.Target.copy_tags = dt.new_widget('check_button'){ + label = _('copy tags'), + value = dt.preferences.read(mod, 'active_copy_tags', 'bool'), + tooltip = _('copy tags from first source image'), + clicked_callback = function(self) dt.preferences.write(mod, 'active_copy_tags', 'bool', self.value) end, + reset_callback = function(self) self.value = true end +} +temp = dt.preferences.read(mod, 'active_add_tags', 'string') +if temp == '' then temp = nil end +GUI.Target.add_tags = dt.new_widget('entry'){ + tooltip = _('additional tags to be added on import, separate with commas, all spaces will be removed'), + text = temp, + placeholder = _('enter tags, separated by commas'), + editable = true +} +GUI.run = dt.new_widget('button'){ + label = _('merge'), + tooltip =_('run HDRMerge with the above settings'), + clicked_callback = function() main() end +} +GUI.exes.HDRMerge = dt.new_widget('file_chooser_button'){ + title = _('select HDRmerge executable'), + value = df.get_executable_path_preference(HDRM.name), + is_directory = false +} +GUI.exes.update = dt.new_widget('button'){ + label = _('update'), + tooltip =_('update the binary path with current value'), + clicked_callback = function() ExeUpdate({HDRM}) end +} +GUI.options = dt.new_widget('box'){ + orientation = 'vertical', + lbl_hdr, + GUI.HDR.bps, + GUI.HDR.size, + GUI.HDR.batch, + GUI.HDR.gap, + lbl_import, + GUI.Target.style, + GUI.Target.copy_tags, + GUI.Target.add_tags, + GUI.run +} +local exes_box = dt.new_widget('box'){ + orientation = 'vertical', + GUI.exes.HDRMerge, + GUI.exes.update +} +GUI.stack = dt.new_widget('stack'){ + GUI.options, + exes_box +} +if dt.preferences.read(mod, 'bin_exists', 'bool') then + GUI.stack.active = 1 +else + GUI.stack.active = 2 +end + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not HDRM.event_registered then + dt.register_event( + "HDRmerge", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + HDRM.event_registered = true + end +end + +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = restart + +return script_data \ No newline at end of file diff --git a/contrib/LabelsToTags.lua b/contrib/LabelsToTags.lua new file mode 100644 index 00000000..32031a3b --- /dev/null +++ b/contrib/LabelsToTags.lua @@ -0,0 +1,296 @@ +--[[ + LABELS TO TAGS + Allows the mass-application of tags using color labels and ratings + as a guide. + + AUTHOR + August Schwerdfeger (august@schwerdfeger.name) + + INSTALLATION + * Copy this file into $CONFIGDIR/lua/, where CONFIGDIR + is your darktable configuration directory + * Add the following line in the file $CONFIGDIR/luarc: + require "LabelsToTags" + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + None. + + USAGE + In your 'luarc' file or elsewhere, use the function + 'register_tag_mapping', defined in this module, to specify + one or more tag mappings for use by the module. + Any mappings so registered will be selectable, according + to their given names, in the module's "mapping" combo box. + + A mapping takes the form of a table mapping patterns to + lists of tags. A pattern consists of 6 characters, of which + the first five represent color labels and the last the rating. + Each color label character may be '+', '-', or '*', + indicating that for this pattern to match, the corresponding + color label, respectively, must be on, must be off, or can be + either. Similarly, the rating character may be a numeral + between 0 and 5, "R" for rejected, or "*" for "any value." + + An example call to 'register_tag_mapping' is provided in a + comment at the end of this file. + + When the "Start" button is pressed, the module will + iterate over each selected image and check the state of + that image's color labels and rating against each pattern + defined in the selected mapping. For each pattern that + matches, the corresponding tags will be added to the + image. Any such tag not already existing in the database + will be created. + + LICENSE + LGPLv2+ + +]] + +local darktable = require("darktable") +local du = require "lib/dtutils" + +du.check_min_api_version("7.0.0", "LabelsToTags") + +local gettext = darktable.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("labels to tags"), + purpose = _("allows the mass-application of tags using color labels and ratings as a guide"), + author = "August Schwerdfeger (august@schwerdfeger.name)", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/LabelsToTags" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +-- Lua 5.3 no longer has "unpack" but "table.unpack" +unpack = unpack or table.unpack + +local ltt = {} +ltt.module_installed = false +ltt.event_registered = false + +local LIB_ID = _("LabelsToTags") + +-- Helper functions: BEGIN + +local function keySet(t) + local rv = {} + for k,_ in pairs(t) do + table.insert(rv,k) + end + table.sort(rv) + return(rv) +end + +local function generateLabelHash(img) + local hash = "" + hash = hash .. (img.red and "+" or "-") + hash = hash .. (img.yellow and "+" or "-") + hash = hash .. (img.green and "+" or "-") + hash = hash .. (img.blue and "+" or "-") + hash = hash .. (img.purple and "+" or "-") + hash = hash .. (img.rating == -1 and "R" or tostring(img.rating)) + return(hash) +end + +local function hashMatch(hash,pattern) + if #(hash) ~= #(pattern) then return(false) end + for i = 0,#hash do + if string.sub(hash,i,i) ~= string.sub(pattern,i,i) and + string.sub(pattern,i,i) ~= "*" then + return(false) + end + end + return(true) +end + +-- Helper functions: 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") } } +} + +local availableMappings = {} + +local function getAvailableMappings() + if availableMappings == nil or next(availableMappings) == nil then + return(initialAvailableMappings) + else + return(availableMappings) + end +end + +local function getComboboxTooltip() + if availableMappings == nil or next(availableMappings) == nil then + return(_("no registered mappings -- using defaults")) + else + return(_("select a label-to-tag mapping")) + end +end + +local mappingComboBox = darktable.new_widget("combobox"){ + label = _("mapping"), + value = 1, + tooltip = getComboboxTooltip(), + reset_callback = function(selfC) + if selfC == nil then + return + end + i = 1 + for _,m in pairs(keySet(getAvailableMappings())) do + selfC[i] = m + i = i+1 + end + n = #selfC + for j = i,n do + selfC[i] = nil + end + selfC.value = 1 + selfC.tooltip = getComboboxTooltip() + end, + unpack(keySet(getAvailableMappings())) +} + +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) + job.percent = 0.0 + local pctIncrement = 1.0 / #(darktable.gui.action_images) + + local availableMappings = getAvailableMappings() + local memoizedTags = {} + for _,img in ipairs(darktable.gui.action_images) do + local tagsToApply = {} + local hash = generateLabelHash(img) + for k,v in pairs(availableMappings[mappingComboBox.value]) do + if hashMatch(hash,k) then + for _,tag in ipairs(v) do + tagsToApply[tag] = true + end + end + end + for k,_ in pairs(tagsToApply) do + if memoizedTags[k] == nil then + memoizedTags[k] = darktable.tags.create(k) + end + darktable.tags.attach(memoizedTags[k],img) + end + job.percent = job.percent + pctIncrement + end + job.valid = false +end + +ltt.my_widget = darktable.new_widget("box") { + orientation = "vertical", + mappingComboBox, + darktable.new_widget("button") { + label = _("start"), + tooltip = _("tag all selected images"), + clicked_callback = doTagging + } +} + +local PATTERN_PATTERN = "^[+*-][+*-][+*-][+*-][+*-][0-5R*]$" + +darktable.register_tag_mapping = function(name, mapping) + if availableMappings[name] ~= nil then + darktable.print_error("Tag mapping '" .. name .. "' already registered") + return + end + for pattern,tags in pairs(mapping) do + if string.match(pattern,PATTERN_PATTERN) == nil then + darktable.print_error("In tag mapping '" .. name .. "': Pattern '" .. pattern .. "' invalid") + return + end + for _,tag in ipairs(tags) do + if type(tag) ~= "string" then + darktable.print_error("In tag mapping '" .. name .. "': All tag mappings must be lists of strings") + return + end + end + end + availableMappings[name] = mapping + mappingComboBox.reset_callback(mappingComboBox) +end + +local function install_module() + if not ltt.module_installed then + 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 + end +end + +local function destroy() + darktable.gui.libs[LIB_ID].visible = false +end + +local function restart() + darktable.gui.libs[LIB_ID].visible = true +end + +--[[ +darktable.register_tag_mapping("Example", + { ["+----*"] = { "Red", "Only red" }, + ["-+---*"] = { "Yellow", "Only yellow" }, + ["--+--*"] = { "Green", "Only green" }, + ["---+-*"] = { "Blue", "Only blue" }, + ["****+*"] = { "Purple" }, + ["----+*"] = { "Only purple" }, + ["*****1"] = { "One star" }, + ["*****R"] = { "Rejected" } }) +]] + +if darktable.gui.current_view().id == "lighttable" then + install_module() +else + if not ltt.event_registered then + darktable.register_event( + LIB_ID, "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + ltt.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 diff --git a/contrib/OpenInExplorer.lua b/contrib/OpenInExplorer.lua new file mode 100644 index 00000000..14f788d7 --- /dev/null +++ b/contrib/OpenInExplorer.lua @@ -0,0 +1,261 @@ +--[[ +OpenInExplorer plugin for darktable + + copyright (c) 2018 Kevin Ertel + Update 2020 and macOS support by Volker Lenhardt + Linux support 2020 by Bill Ferguson + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +]] + +--[[About this plugin +This plugin adds the module "OpenInExplorer" to darktable's lighttable view. + +----REQUIRED SOFTWARE---- +Apple macOS, Microsoft Windows or Linux + +----USAGE---- +Install: (see here for more detail: https://github.com/darktable-org/lua-scripts ) + 1) Copy this file into your "lua/contrib" folder where all other scripts reside. + 2) Require this file in your luarc file, as with any other dt plug-in + +Select the photo(s) you wish to find in your operating system's file manager and press "show in file explorer" in the "selected images" section. + +- Nautilus (Linux), Explorer (Windows), and Finder (macOS prior to Mojave) will open one window for each selected image at the file's location. The file name will be highlighted. + +- On macOS Mojave and Catalina the Finder will open one window for each different directory. In these windows only the last one of the corresponding files will be highlighted (bug or feature?). + +- Dolphin (Linux) will open one window with tabs for the different directories. All the selected images' file names are highlighted in their respective directories. + +As an alternative option you can choose to show the image file names as symbolic links in an arbitrary directory. Go to preferences|Lua options. This option is not available for Windows users as on Windows solely admins are allowed to create links. + +- Pros: You do not clutter up your display with multiple windows. So there is no need to limit the number of selections. + +- Cons: If you want to work with the files you are one step behind the original data. + +----KNOWN ISSUES---- +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local dsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext + +--Check API version +du.check_min_api_version("7.0.0", "OpenInExplorer") + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("open in explorer"), + purpose = _("open a selected file in the system file manager"), + author = "Kevin Ertel", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/OpenInExplorer" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +local act_os = dt.configuration.running_os +local PS = act_os == "windows" and "\\" or "/" + +--Detect OS and quit if it is not supported. +if act_os ~= "macos" and act_os ~= "windows" and act_os ~= "linux" then + dt.print(_("OpenInExplorer plug-in only supports linux, macos, and windows at this time")) + dt.print_error("OpenInExplorer plug-in only supports Linux, macOS, and Windows at this time") + return +end + +local use_links, links_dir = false, "" +if act_os ~= "windows" then + use_links = dt.preferences.read("OpenInExplorer", "use_links", "bool") + links_dir = dt.preferences.read("OpenInExplorer", "linked_image_files_dir", "string") +end + +--Check if the directory exists that was chosen for the file links. Return boolean. +local function check_if_links_dir_exists() + local dir_exists = true + if not links_dir then + --Just for paranoic reasons. I tried, but I couldn't devise a setting for a nil value. + dt.print(_("no links directory selected\nplease check the dt preferences (lua options)")) + dt.print_error("OpenInExplorer: No links directory selected") + dir_exists = false + elseif not df.check_if_file_exists(links_dir) then + dt.print(string.format(_("links directory '%s' not found\nplease check the dt preferences (lua options)"), links_dir)) + dt.print_error(string.format("OpenInExplorer: Links directory '%s' not found", links_dir)) + dir_exists = false + end + return dir_exists +end + +--Format strings for the commands to open the corresponding OS's file manager. +local open_dir = {} +open_dir.windows = "explorer.exe /n, %s" +open_dir.macos = "open %s" +open_dir.linux = [[busctl --user call org.freedesktop.FileManager1 /org/freedesktop/FileManager1 org.freedesktop.FileManager1 ShowFolders ass 1 %s ""]] + +local open_files = {} +open_files.windows = "explorer.exe /select, %s" +open_files.macos = "osascript -e 'tell application \"Finder\" to (reveal {%s}) activate'" +open_files.linux = [[busctl --user call org.freedesktop.FileManager1 /org/freedesktop/FileManager1 org.freedesktop.FileManager1 ShowItems ass %d %s ""]] + +reveal_file_osx_cmd = "\"%s\" as POSIX file" + +--Call the osx Finder with each selected image selected. +--For images in multiple folders, Finder will open a separate window for each +local function call_list_of_files_osx(selected_images) + local cmds = {} + for _, image in pairs(selected_images) do + current_image = image.path..PS..image.filename + -- AppleScript needs double quoted strings, and the whole command is wrapped in single quotes. + table.insert(cmds, string.format(reveal_file_osx_cmd, string.gsub(string.gsub(current_image, "\"", "\\\""), "'", "'\"'\"'"))) + end + reveal_cmd = table.concat(cmds, ",") + run_cmd = string.format(open_files.macos, reveal_cmd) + dt.print_log("OSX run_cmd = "..run_cmd) + dsys.external_command(run_cmd) +end + +--Call the file mangager for each selected image on Linux. +--There is one call to busctl containing a list of all the image file names. +local function call_list_of_files(selected_images) + local current_image, file_uris, run_cmd = "", "", "" + for _, image in pairs(selected_images) do + current_image = image.path..PS..image.filename + file_uris = file_uris .. df.sanitize_filename("file://" .. current_image) .. " " + dt.print_log("file_uris is " .. file_uris) + end + run_cmd = string.format(open_files.linux, #selected_images, file_uris) + dt.print_log("OpenInExplorer run_cmd = "..run_cmd) + dsys.external_command(run_cmd) +end + +--Call the file manager for each selected image on Windows and macOS. +local function call_file_by_file(selected_images) + local current_image, run_cmd = "", "" + for _, image in pairs(selected_images) do + current_image = image.path..PS..image.filename + run_cmd = string.format(open_files[act_os], df.sanitize_filename(current_image)) + dt.print_log("OpenInExplorer run_cmd = "..run_cmd) + dsys.external_command(run_cmd) + end +end + +--Create a link for each selected image, and finally call the file manager. +local function set_links(selected_images) + local current_image, link_target, run_cmd, k = "", "", "", nil + for k, image in pairs(selected_images) do + current_image = image.path..PS..image.filename + link_target = df.create_unique_filename(links_dir .. PS .. image.filename) + run_cmd = string.format("ln -s %s %s", df.sanitize_filename(current_image), df.sanitize_filename(link_target)) + --[[ + In case Windows will allow normal users to create soft links: + if act_os == "windows" then + run_cmd = string.format("mklink %s %s", df.sanitize_filename(link_target), df.sanitize_filename(current_image)) + end + ]] + if dsys.external_command(run_cmd) ~= 0 then + dt.print(_("failed to create links, missing rights?")) + dt.print_error("OpenInExplorer: Failed to create links") + return + end + end + --The URI format is necessary only for the Linux busctl command. + --But it is accepted by the Windows Explorer and macOS's Finder all the same. + run_cmd = string.format(open_dir[act_os], df.sanitize_filename("file://"..links_dir)) + dt.print_log("OpenInExplorer run_cmd = "..run_cmd) + dsys.external_command(run_cmd) +end + +--The working function that starts the particular task. +local function open_in_fmanager() + local images = dt.gui.selection() + if #images == 0 then + dt.print(_("please select an image")) + else + if use_links and not check_if_links_dir_exists() then + return + end + if #images > 15 and not use_links then + dt.print(_("please select fewer images (max. 15)")) + elseif use_links then + set_links(images) + else + if act_os == "linux" then + call_list_of_files(images) + elseif act_os == "macos" then + call_list_of_files_osx(images) + else + call_file_by_file(images) + end + end + end +end + +local function destroy() + dt.gui.libs.image.destroy_action("OpenInExplorer") + dt.destroy_event("OpenInExplorer", "shortcut") + if act_os ~= "windows" then + dt.preferences.destroy("OpenInExplorer", "linked_image_files_dir") + end + dt.preferences.destroy("OpenInExplorer", "use_links") +end + + +-- GUI -- +dt.gui.libs.image.register_action( + "OpenInExplorer", _("show in file explorer"), + function() open_in_fmanager() end, + _("open the file manager at the selected image's location") +) + + +if act_os ~= "windows" then + dt.preferences.register("OpenInExplorer", "linked_image_files_dir", -- name + "directory", -- type + _("OpenInExplorer: linked files directory"), -- label + _("directory to store the links to the file names, requires restart to take effect"), -- tooltip + "Links to image files", -- default + dt.new_widget("file_chooser_button"){ + title = _("select directory"), + is_directory = true, + } + ) + dt.preferences.register("OpenInExplorer", "use_links", -- name + "bool", -- type + _("OpenInExplorer: use links"), -- label + _("use links instead of multiple windows, requires restart to take effect"), -- tooltip + false, -- default + "" + ) +end + +dt.register_event( + "OpenInExplorer", "shortcut", + function(event, shortcut) open_in_fmanager() end, + "OpenInExplorer" +) + +script_data.destroy = destroy + +return script_data diff --git a/contrib/RL_out_sharp.lua b/contrib/RL_out_sharp.lua new file mode 100644 index 00000000..b65581c9 --- /dev/null +++ b/contrib/RL_out_sharp.lua @@ -0,0 +1,272 @@ +--[[ + Richardson-Lucy output sharpening for darktable using GMic + + 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 . +]] + +--[[ + DESCRIPTION + RL_out_sharp.lua - Richardson-Lucy output sharpening using GMic + + This script provides a new target storage "RL output sharpen". + Images exported will be sharpened using GMic (RL deblur algorithm) + + REQUIRED SOFTWARE + GMic command line interface (CLI) https://gmic.eu/download.shtml + + USAGE + * require this script from main lua file + * in lua preferences, select the GMic cli executable + * from "export selected", choose "RL output sharpen" + * configure output folder + * configure RL parameters with sliders + * configure temp files format and quality, jpg 8bpp (good quality) + and tif 16bpp (best quality) are supported + * configure other export options (size, etc.) + * export, images will be first exported in the temp format, then sharpened + * sharpened images will be stored in jpg format in the output folder + + EXAMPLE + set sigma = 0.7, iterations = 10, jpeg output quality = 95, + to correct blur due to image resize for web usage + + CAVEATS + MAC compatibility not tested + Although Darktable can handle file names containing spaces, GMic cli cannot, + so if you want to use this script please make sure that your images do not + have spaces in the file name and path + + BUGS, COMMENTS, SUGGESTIONS + send to Marco Carrarini, marco.carrarini@gmail.com + + CHANGES + * 20200308 - initial version +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local dtsys = require "lib/dtutils.system" + +-- module name +local MODULE_NAME = "RL_out_sharp" + +-- check API version +du.check_min_api_version("7.0.0", MODULE_NAME) + +-- translation +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) + end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("RL output sharpening"), + purpose = _("Richardson-Lucy output sharpening using GMic"), + author = "Marco Carrarini ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/RL_out_sharp" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +-- OS compatibility +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +-- 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 .. " exiftool not found, metadata not preserved") + end +end + + +-- setup export --------------------------------------------------------------- +local function setup_export(storage, img_format, image_table, high_quality, extra_data) + -- set 16bpp if format is tif + if img_format.extension == "tif" then + img_format.bpp = 16 + end + end + + +-- temp export formats: jpg and tif are supported ----------------------------- +local function supported(storage, img_format) + return (img_format.extension == "jpg") or (img_format.extension == "tif") + end + + +-- export and sharpen images -------------------------------------------------- +local function export2RL(storage, image_table, extra_data) + + local temp_name, new_name, run_cmd, result + local input_file, output_file, options + + -- read parameters + local gmic = dt.preferences.read(MODULE_NAME, "gmic_exe", "string") + if gmic == "" then + dt.print(_("GMic executable not configured")) + return + end + gmic = df.sanitize_filename(gmic) + local output_folder = output_folder_selector.value + local sigma_str = string.gsub(string.format("%.2f", sigma_slider.value), ",", ".") + local iterations_str = string.format("%.0f", iterations_slider.value) + local jpg_quality_str = string.format("%.0f", jpg_quality_slider.value) + + -- save preferences + dt.preferences.write(MODULE_NAME, "sigma", "string", sigma_str) + dt.preferences.write(MODULE_NAME, "iterations", "string", iterations_str) + dt.preferences.write(MODULE_NAME, "jpg_quality", "string", jpg_quality_str) + + local gmic_operation = " -deblur_richardsonlucy "..sigma_str..","..iterations_str..",1" + + local i = 0 + for image, temp_name in pairs(image_table) do + + i = i + 1 + dt.print(string.format(_("sharpening image %d ..."), i)) + -- create unique filename + new_name = output_folder..PS..df.get_basename(temp_name)..".jpg" + new_name = df.create_unique_filename(new_name) + + -- build the GMic command string + input_file = df.sanitize_filename(temp_name) + output_file = df.sanitize_filename(new_name) + options = " cut 0,255 round " + if df.get_filetype(temp_name) == "tif" then options = " -/ 256"..options end + + run_cmd = gmic.." "..input_file..gmic_operation..options.."o "..output_file..","..jpg_quality_str + + result = dtsys.external_command(run_cmd) + if result ~= 0 then + dt.print(_("sharpening error")) + return + end + + -- copy metadata from input_file to output_file + preserve_metadata(input_file, output_file) + + -- delete temp image + os.remove(temp_name) + + end + + dt.print(_("finished exporting")) + end + + -- script_manager integration + + local function destroy() + dt.destroy_storage("exp2RL") + end + +-- new widgets ---------------------------------------------------------------- + +output_folder_selector = dt.new_widget("file_chooser_button"){ + title = _("select output folder"), + tooltip = _("select output folder"), + value = dt.preferences.read(MODULE_NAME, "output_folder", "string"), + is_directory = true, + changed_callback = function(self) + dt.preferences.write(MODULE_NAME, "output_folder", "string", self.value) + end + } + +sigma_slider = dt.new_widget("slider"){ + label = _("sigma"), + tooltip = _("controls the width of the blur that's applied"), + soft_min = 0.3, + soft_max = 2.0, + hard_min = 0.0, + hard_max = 3.0, + step = 0.05, + digits = 2, + value = 1.0 + } + +iterations_slider = dt.new_widget("slider"){ + label = _("iterations"), + tooltip = _("increase for better sharpening, but slower"), + soft_min = 0, + soft_max = 100, + hard_min = 0, + hard_max = 100, + step = 5, + digits = 0, + value = 10.0 + } + +jpg_quality_slider = dt.new_widget("slider"){ + label = _("output jpg quality"), + tooltip = _("quality of the output jpg file"), + soft_min = 70, + soft_max = 100, + hard_min = 70, + hard_max = 100, + step = 2, + digits = 0, + value = 95.0 + } + +local storage_widget = dt.new_widget("box"){ + orientation = "vertical", + output_folder_selector, + sigma_slider, + iterations_slider, + jpg_quality_slider + } + +-- register new storage ------------------------------------------------------- +dt.register_storage("exp2RL", _("RL output sharpen"), nil, export2RL, supported, save_preferences, storage_widget) + +-- register the new preferences ----------------------------------------------- +dt.preferences.register(MODULE_NAME, "gmic_exe", "file", +_("executable for GMic CLI"), +_("select executable for GMic command line version") , "") + +-- set sliders to the last used value at startup ------------------------------ +sigma_slider.value = dt.preferences.read(MODULE_NAME, "sigma", "float") +iterations_slider.value = dt.preferences.read(MODULE_NAME, "iterations", "float") +jpg_quality_slider.value = dt.preferences.read(MODULE_NAME, "jpg_quality", "float") + +-- script_manager integration + +script_data.destroy = destroy + +return script_data + +-- end of script -------------------------------------------------------------- + +-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua +-- kate: hl Lua; diff --git a/contrib/auto_snapshot.lua b/contrib/auto_snapshot.lua new file mode 100644 index 00000000..48f99f5b --- /dev/null +++ b/contrib/auto_snapshot.lua @@ -0,0 +1,132 @@ +--[[ + + auto_snapshot.lua - automatically take a snapshot when an image is loaded in darkroom + + Copyright (C) 2024 Bill Ferguson . + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +]] +--[[ + auto_snapshot - + + automatically take a snapshot when an image is loaded in darkroom + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + None + + USAGE + * start the script from script_manager + * open an image in darkroom + + BUGS, COMMENTS, SUGGESTIONS + Bill Ferguson + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local log = require "lib/dtutils.log" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "auto_snapshot" +local DEFAULT_LOG_LEVEL = log.error + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("7.0.0", MODULE) -- choose the minimum version that contains the features you need + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- I 1 8 N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- S C R I P T M A N A G E R I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local script_data = {} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +script_data.metadata = { + name = _("auto snapshot"), -- name of script + purpose = _("automatically take a snapshot when an image is loaded in darkroom"), -- purpose of script + author = "Bill Ferguson ", -- your name and optionally e-mail address + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/auto_snapshot/" -- URL to help/documentation +} + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- L O G L E V E L +-- - - - - - - - - - - - - - - - - - - - - - - - + +log.log_level(DEFAULT_LOG_LEVEL) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local auto_snapshot = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- P R E F E R E N C E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.preferences.register(MODULE, "always_create_snapshot", "bool", "auto_snapshot - " .. _("always automatically create_snapshot"), + _("create a snapshot even if the image is altered"), false) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + dt.destroy_event(MODULE, "darkroom-image-loaded") +end + +script_data.destroy = destroy + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.register_event(MODULE, "darkroom-image-loaded", + function(event, clean, image) + local always = dt.preferences.read(MODULE, "always_create_snapshot", "bool") + if clean and always then + dt.gui.libs.snapshots.take_snapshot() + elseif clean and not image.is_altered then + dt.gui.libs.snapshots.take_snapshot() + end + + end +) + + +return script_data \ No newline at end of file diff --git a/contrib/autostyle.lua b/contrib/autostyle.lua index 66eca201..32add29b 100644 --- a/contrib/autostyle.lua +++ b/contrib/autostyle.lua @@ -37,94 +37,41 @@ 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") --- Forward declare the functions -local autostyle_apply_one_image,autostyle_apply_one_image_event,autostyle_apply,exiftool_attribute,capture +local gettext = darktable.gettext.gettext --- Tested it with darktable 1.6.1 and darktable git from 2014-01-25 -darktable.configuration.check_version(...,{2,0,2},{2,1,0},{3,0,0},{4,0,0},{5,0,0}) - --- Receive the event triggered -function autostyle_apply_one_image_event(event,image) - autostyle_apply_one_image(image) +local function _(msgid) + return gettext(msgid) end --- Apply the style to an image, if it matches the tag condition -function autostyle_apply_one_image (image) - -- We need the tag, the value and the style_name provided from the configuration string - local tag,value,style_name=string.match(darktable.preferences.read("autostyle","exif_tag","string"),"(%g+)%s*=%s*(%g+)%s*=>%s*(%g+)") +-- return data structure for script_manager - -- check they all exist (correct syntax) - if (not tag) then - darktable.print("EXIF TAG not found in " .. darktable.preferences.read("autostyle","exif_tag","string")) - return - end - if (not value) then - darktable.print("value to match not found in " .. darktable.preferences.read("autostyle","exif_tag","string")) - return - end - if (not style_name) then - darktable.print("style name not found in " .. darktable.preferences.read("autostyle","exif_tag","string")) - return - end - if not checkIfBinExists("exiftool") then - return - 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) - end +local script_data = {} - -- Apply the style to image, if it is tagged - local ok,auto_dr_attr= pcall(exiftool_attribute,image.path .. '/' .. image.filename,tag) - -- If the lookup fails, stop here - if (not ok) then - return - end - if auto_dr_attr==value then --- darktable.print("Image " .. image.filename .. ": autostyle automatically applied " .. darktable.preferences.read("autostyle","exif_tag","string") ) - darktable.styles.apply(style,image) --- else --- darktable.print("Image " .. image.filename .. ": autostyle not applied, exif tag " .. darktable.preferences.read("autostyle","exif_tag","string") .. " not matched: " .. auto_dr_attr) - end -end +local have_not_printed_config_message = true +script_data.metadata = { + name = _("auto style"), + purpose = _("automatically apply a style based on image EXIF tag"), + author = "Marc Cousin ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/autostyle/" +} -function autostyle_apply( shortcut) - local images = darktable.gui.action_images - for _,image in pairs(images) do - autostyle_apply_one_image(image) - end -end - --- Retrieve the attribute through exiftool -function exiftool_attribute(path,attr) - local cmd="exiftool -" .. attr .. " '" ..path.. "'"; - local exifresult=get_stdout(cmd) - local attribute=string.match(exifresult,": (.*)") - if (attribute == nil) then - darktable.print( "Could not find the attribute " .. attr .. " using the command: <" .. cmd .. ">") - -- Raise an error to the caller - error( "Could not find the attribute " .. attr .. " using the command: <" .. cmd .. ">"); - end - return attribute -end +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them -- run command and retrieve stdout -function get_stdout(cmd) +local function get_stdout(cmd) -- Open the command, for reading local fd = assert(io.popen(cmd, 'r')) - dt.control.read(fd) + darktable.control.read(fd) -- slurp the whole file local data = assert(fd:read('*a')) @@ -138,12 +85,116 @@ function get_stdout(cmd) return data end +-- Retrieve the attribute through exiftool +local function exiftool_attribute(path, attr) + local cmd = "exiftool -" .. attr .. " '" .. path .. "'"; + local exifresult = get_stdout(cmd) + local attribute = string.match(exifresult, ": (.*)") + if (attribute == nil) then + darktable.print_error( "Could not find the attribute " .. attr .. " using the command: <" .. cmd .. ">") + -- Raise an error to the caller + error( "Could not find the attribute " .. attr .. " using the command: <" .. cmd .. ">"); + end + -- darktable.print_error("Returning attribute: " .. attribute) + return attribute +end + +-- Apply the style to an image, if it matches the tag condition +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+)") + + -- check they all exist (correct syntax) + if (not tag) then + darktable.print(string.format(_("EXIF tag not found in %s"), pref)) + return 0 + end + if (not value) then + darktable.print(string.format(_("value to match not found in %s"), pref)) + return 0 + end + if (not style_name) then + darktable.print(string.format(_("style name not found in %s"), pref)) + return 0 + end + if not filelib.check_if_bin_exists("exiftool") then + darktable.print(_("can't find exiftool")) + return 0 + end + + + -- First find the style (we have its name) + local styles = darktable.styles + local style + for _, s in ipairs(styles) do + if s.name == style_name then + style = s + end + end + if (not style) then + darktable.print(string.format(_("style not found for autostyle: %s"), style_name)) + return 0 + end + + -- Apply the style to image, if it is tagged + local ok, auto_dr_attr = pcall(exiftool_attribute, image.path .. '/' .. image.filename,tag) + --darktable.print_error("dr_attr:" .. auto_dr_attr) + -- If the lookup fails, stop here + if (not ok) then + darktable.print(string.format(_("couldn't get attribute %s from exiftool's output"), auto_dr_attr)) + return 0 + end + if auto_dr_attr == value then + darktable.print_log("Image " .. image.filename .. ": autostyle automatically applied " .. pref) + darktable.styles.apply(style,image) + return 1 + else + darktable.print_log("Image " .. image.filename .. ": autostyle not applied, exif tag " .. pref .. " not matched: " .. auto_dr_attr) + return 0 + end + elseif have_not_printed_config_message then + have_not_printed_config_message = false + darktable.print(string.format(_("%s is not configured, please configure the preference in Lua options"), script_data.metadata.name)) + end +end + + +-- Receive the event triggered +local function autostyle_apply_one_image_event(event,image) + autostyle_apply_one_image(image) +end + +local function autostyle_apply(shortcut) + local images = darktable.gui.action_images + local images_processed = 0 + local images_submitted = 0 + for _,image in pairs(images) do + 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)) +end + +local function destroy() + darktable.destroy_event("autostyle", "shortcut") + darktable.destroy_event("autostyle", "post-import-image") +end + -- Registering events -darktable.register_event("shortcut",autostyle_apply, - "Apply your chosen style from exiftool tags") +darktable.register_event("autostyle", "shortcut", autostyle_apply, + _("apply your chosen style from exiftool tags")) -darktable.preferences.register("autostyle","exif_tag","string","Autostyle: EXIF_tag=value=>style","apply a style automatically if an EXIF_tag matches value. Find the tag with exiftool","") +darktable.preferences.register("autostyle", "exif_tag", "string", + string.format("%s: EXIF_tag=value=>style", script_data.metadata.name), + _("apply a style automatically if an EXIF tag matches value, find the tag with exiftool"), "") -darktable.register_event("post-import-image",autostyle_apply_one_image_event) +darktable.register_event("autostyle", "post-import-image", + autostyle_apply_one_image_event) +script_data.destroy = destroy +return script_data diff --git a/contrib/change_group_leader.lua b/contrib/change_group_leader.lua new file mode 100644 index 00000000..0e6f66e6 --- /dev/null +++ b/contrib/change_group_leader.lua @@ -0,0 +1,205 @@ +--[[ + change group leader + + copyright (c) 2020, 2022 Bill Ferguson + copyright (c) 2021 Angel Angelov + + 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 . +]] + +--[[ +CHANGE GROUP LEADER +automatically change the leader of raw+jpg paired image groups + +INSTALLATION +* copy this file in $CONFIGDIR/lua/ where CONFIGDIR is your darktable configuration directory +* add the following line in the file $CONFIGDIR/luarc + require "change_group_leader" + +USAGE +* in lighttable mode, select the image groups you wish to process, + select whether you want to set the leader to "jpg" or "raw", + and click "Execute" +]] + +local dt = require "darktable" +local du = require "lib/dtutils" + +local gettext = dt.gettext.gettext + +local MODULE = "change_group_leader" + +du.check_min_api_version("3.0.0", MODULE) + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("change group leader"), + purpose = _("automatically change the leader of raw+jpg paired image groups"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/change_group_leader" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +-- create a namespace to contain persistent data and widgets +local chg_grp_ldr = {} + +local cgl = chg_grp_ldr + +cgl.widgets = {} + +cgl.event_registered = false +cgl.module_installed = false + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function install_module() + if not cgl.module_installed then + dt.register_lib( + MODULE, -- Module name + _("change group leader"), -- Visible name + true, -- expandable + false, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 700}}, -- containers + cgl.widgets.box, + nil,-- view_enter + nil -- view_leave + ) + cgl.module_installed = true + end +end + +local function find_group_leader(images, mode) + for _, img in ipairs(images) do + dt.print_log("checking image " .. img.id .. " named " .. img.filename) + local found = false + if mode == "jpg" then + if string.match(string.lower(img.filename), "jpg$") then + dt.print_log("jpg matched image " .. img.filename) + found = true + end + elseif mode == "raw" then + if img.is_raw and img.duplicate_index == 0 then + dt.print_log("found raw " .. img.filename) + found = true + end + elseif mode == "non-raw" then + if img.is_ldr then + dt.print_log("found ldr " .. img.filename) + found = true + end + else + dt.print_error(MODULE .. ": unrecognized mode " .. mode) + return + end + if found then + dt.print_log("making " .. img.filename .. " group leader") + img:make_group_leader() + return + end + end +end + +local function process_image_groups(images) + if #images < 1 then + dt.print(_("no images selected")) + dt.print_log(MODULE .. "no images seletected, returning...") + else + local mode = cgl.widgets.mode.value + for _,img in ipairs(images) do + dt.print_log("checking image " .. img.id) + local group_images = img:get_group_members() + if group_images == 1 then + dt.print_log("only one image in group for image " .. image.id) + else + find_group_leader(group_images, mode) + end + end + end +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 function destroy() + dt.gui.libs[MODULE].visible = false +end + +local function restart() + dt.gui.libs[MODULE].visible = true +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- W I D G E T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +cgl.widgets.mode = dt.new_widget("combobox"){ + label = _("select new group leader"), + tooltip = _("select type of image to be group leader"), + selected = 1, + "jpg", "raw", "non-raw", +} + +cgl.widgets.execute = dt.new_widget("button"){ + label = _("execute"), + clicked_callback = function() + process_image_groups(dt.gui.action_images) + end +} + +cgl.widgets.box = dt.new_widget("box"){ + orientation = "vertical", + cgl.widgets.mode, + cgl.widgets.execute, +} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not cgl.event_registered then + dt.register_event( + "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + cgl.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 diff --git a/contrib/clear_GPS.lua b/contrib/clear_GPS.lua index bc5fc4e3..2663ab4b 100644 --- a/contrib/clear_GPS.lua +++ b/contrib/clear_GPS.lua @@ -1,6 +1,6 @@ --[[ - clear_GPS.lua - export and edit with GIMP + clear_GPS.lua - plugin for Darktable Copyright (C) 2016 Bill Ferguson . @@ -37,20 +37,34 @@ ]] local dt = require "darktable" -local gettext = dt.gettext +local du = require "lib/dtutils" --- not a number -local NaN = 0/0 +local gettext = dt.gettext.gettext -dt.configuration.check_version(...,{3,0,0},{4,0,0}) +local function _(msgid) + return gettext(msgid) +end +-- return data structure for script_manager --- 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 script_data = {} -local function _(msgid) - return gettext.dgettext("clear_GPS", msgid) -end +script_data.metadata = { + name = _("clear GPS info"), + purpose = _("remove GPS data from selected image(s)"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/clear_gps/" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +-- not a number +local NaN = 0/0 + +du.check_min_api_version("7.0.0", "clear_GPS") local function clear_GPS(images) for _, image in ipairs(images) do @@ -61,15 +75,23 @@ local function clear_GPS(images) end end +local function destroy() + dt.destroy_event("clear_GPS", "shortcut") + dt.gui.libs.image.destroy_action("clear_GPS") +end + +script_data.destroy = destroy dt.gui.libs.image.register_action( - _("clear GPS data"), + "clear_GPS", _("clear GPS data"), function(event, images) clear_GPS(images) end, - "clear GPS data" + _("clear GPS data from selected images") ) dt.register_event( - "shortcut", + "clear_GPS", "shortcut", function(event, shortcut) clear_GPS(dt.gui.action_images) end, - "clear GPS data" + _("clear GPS data from selected images") ) + +return script_data diff --git a/contrib/color_profile_manager.lua b/contrib/color_profile_manager.lua new file mode 100644 index 00000000..683ae990 --- /dev/null +++ b/contrib/color_profile_manager.lua @@ -0,0 +1,377 @@ +--[[ + + color_profile_manager.lua - manage external darktable color profiles + + Copyright (C) 2021 Bill Ferguson . + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +]] +--[[ + color_profile_manager - manage external darktable color profiles + + This script provides a tool to manage input and output external color + profiles used by darktable. Color profiles can be added or removed + to/from the correct directories so that darktable can find and use + them. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + * None + + USAGE + * require this script from your main lua file + * select the input or output set of color profiles + * a list of profiles is displayed. Click the check box beside the + profile name to select it for removal. Click the "remove profile" + button to remove the profile. + * use the file selector to select a color profile to add to the currently + selected set (input or output) + * click the "add profile" button to add the profile to the selected set + + BUGS, COMMENTS, SUGGESTIONS + * Send to Bill Ferguson, wpferguson@gmail.com or raise an issue on + https://github.com/dakrtable-org/lua-scripts +]] +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") + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- L O C A L I Z A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE_NAME = "color_profile_manager" +local PS = dt.configuration.running_os == "windows" and "\\" or "/" +local CS = dt.configuration.running_os == "windows" and "&" or ";" +local DIR_CMD = dt.configuration.running_os == "windows" and "dir /b" or "ls" +local COLOR_IN_DIR = dt.configuration.config_dir .. PS .. "color" .. PS .. "in" +local COLOR_OUT_DIR = dt.configuration.config_dir .. PS .. "color" .. PS .. "out" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local cpm = {} +cpm.widgets = {} +cpm.widgets.profile_box = dt.new_widget("box"){ + orientation = "vertical", +} +cpm.module_installed = false +cpm.event_registered = false +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function check_for_directories() + if not df.test_file(COLOR_IN_DIR, "d") then + dt.print_log("didn't find " .. COLOR_IN_DIR) + return false + elseif not df.test_file(COLOR_OUT_DIR, "d") then + dt.print_log("didn't find " .. COLOR_OUT_DIR) + return false + else + dt.print_log("both directories exist") + return true + end +end + +local function add_directories() + df.mkdir(COLOR_IN_DIR) + df.mkdir(COLOR_OUT_DIR) +end + +local function list_profiles(dir) + local files = {} + local p = io.popen(DIR_CMD .. " " .. dir) + if p then + for line in p:lines() do + table.insert(files, line) + end + end + p:close() + return files +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_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_log("color profile " .. file .. " removed from " .. dir) +end + +local function clear_profile_box() + for i = #cpm.widgets.profile_box, 1, -1 do + cpm.widgets.profile_box[i] = nil + end +end + +local function add_profile_callback() + local profile_dir = COLOR_IN_DIR + if cpm.widgets.profile_set.selected == 2 then + profile_dir = COLOR_OUT_DIR + end + + local new_profile = cpm.widgets.profile_selector.value + if string.lower(df.get_filetype(new_profile)) ~= "icm" and string.lower(df.get_filetype(new_profile)) ~= "icc" then + dt.print(_("ERROR: color profile must be an icc or icm file")) + dt.print_error(new_profile .. " selected and isn't an icc or icm file") + return + end + + -- set selector value to directory that new profile came from + -- in case there are more + cpm.widgets.profile_selector.value = df.get_path(cpm.widgets.profile_selector.value) + add_profile(new_profile, profile_dir) + local files = list_profiles(profile_dir) + local widgets = {} + + local profile_ptr = 1 + for i, file in ipairs(files) do + if #cpm.widgets.profile_box == 0 or cpm.widgets.profile_box[profile_ptr].label ~= file then + table.insert(widgets, dt.new_widget("check_button"){value = false, label = file}) + else + table.insert(widgets, cpm.widgets.profile_box[profile_ptr]) + profile_ptr = profile_ptr + 1 + if profile_ptr > #cpm.widgets.profile_box then + profile_ptr = #cpm.widgets.profile_box + end + end + end + clear_profile_box() + for _, widget in ipairs(widgets) do + table.insert(cpm.widgets.profile_box, widget) + end + if not cpm.widgets.remove_box.visible then + cpm.widgets.remove_box.visible = true + end +end + +local function remove_profile_callback() + local widgets_to_keep = {} + local profile_dir = COLOR_IN_DIR + if cpm.widgets.profile_set.selected == 2 then + profile_dir = COLOR_OUT_DIR + end + + for _, widget in ipairs(cpm.widgets.profile_box) do + if widget.value == true then + remove_profile(widget.label, profile_dir) + else + table.insert(widgets_to_keep, widget) + end + end + clear_profile_box() + for _, widget in ipairs(widgets_to_keep) do + table.insert(cpm.widgets.profile_box, widget) + end + if #cpm.widgets.profile_box == 0 then + cpm.widgets.remove_box.visible = false + else + cpm.widgets.remove_box.visible = true + end +end + +local function list_profile_callback(choice) + local list_dir = COLOR_IN_DIR + if choice == 2 then + list_dir = COLOR_OUT_DIR + end + + local files = list_profiles(list_dir) + + if #files == 0 then + cpm.widgets.remove_box.visible = false + else + cpm.widgets.remove_box.visible = true + end + + clear_profile_box() + + for i, file in ipairs(files) do + cpm.widgets.profile_box[i] = dt.new_widget("check_button"){value = false, label = file} + end +end + +local function install_module() + if not cpm.module_installed then + dt.register_lib( + MODULE_NAME, -- Module name + _("color profile manager"), -- Visible name + true, -- expandable + false, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 0}}, -- containers + cpm.widgets.main_widget, + nil,-- view_enter + nil -- view_leave + ) + cpm.module_installed = true + dt.control.sleep(500) + if not cpm.initialized then + cpm.widgets.profile_set.visible = false + cpm.widgets.add_box.visible = false + cpm.widgets.remove_box.visible = false + else + cpm.widgets.profile_set.selected = 1 + end + end +end + +local function destroy() + dt.gui.libs[MODULE_NAME].visible = false + return +end + +local function restart() + dt.gui.libs[MODULE_NAME].visible = true + return +end +-- - - - - - - - - - - - - - - - - - - - - - - - +-- W I D G E T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +cpm.initialized = check_for_directories() +dt.print_log("cpm.initialized is " .. tostring(cpm.initialized)) + +if not cpm.initialized then + dt.print_log("creating init_box") + cpm.widgets.init_box = dt.new_widget("box"){ + orientation = "vertical", + dt.new_widget("button"){ + label = _("initialize color profiles"), + tooltip = _("create the directory structure to contain the color profiles"), + clicked_callback = function(this) + add_directories() + cpm.initialized = true + cpm.widgets.profile_set.visible = true + cpm.widgets.add_box.visible = true + cpm.widgets.remove_box.visible = false + cpm.widgets.init_box.visible = false + cpm.widgets.profile_set.selected = 1 + end + } + } +end + +cpm.widgets.profile_set = dt.new_widget("combobox"){ + label = _("select profile set"), + tooltip = _("select input or output profiles"), + visible = cpm.initialized and false or true, + changed_callback = function(this) + if cpm.initialized then + list_profile_callback(this.selected) + end + end, + _("input"), _("output"), +} + +cpm.widgets.profile_selector = dt.new_widget("file_chooser_button"){ + title = _("select color profile to add"), + tooltip = _("select the .icc or .icm file to add"), + is_directory = false +} + +cpm.widgets.add_box = dt.new_widget("box"){ + orientation = "vertical", + visible = cpm.initialized and true or false, + dt.new_widget("section_label"){label = _("add profile")}, + cpm.widgets.profile_selector, + dt.new_widget("button"){ + label = _("add selected color profile"), + tooltip = _("add selected file to profiles"), + clicked_callback = function(this) + add_profile_callback() + end + } +} + +cpm.widgets.remove_box = dt.new_widget("box"){ + orientation = "vertical", + visible = cpm.initialized and true or false, + dt.new_widget("section_label"){label = _("remove profile")}, + cpm.widgets.profile_box, + dt.new_widget("button"){ + label = _("remove selected profile(s)"), + tooltip = _("remove the checked profile(s)"), + clicked_callback = function(this) + remove_profile_callback() + end + } +} + +local main_widgets = {} + +if not cpm.initialized then + table.insert(main_widgets, cpm.widgets.init_box) +end +table.insert(main_widgets, cpm.widgets.profile_set) +table.insert(main_widgets, cpm.widgets.remove_box) +table.insert(main_widgets, cpm.widgets.add_box) + +cpm.widgets.main_widget = dt.new_widget("box"){ + orientation = "vertical", + table.unpack(main_widgets) +} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N +-- - - - - - - - - - - - - - - - - - - - - - - - + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not cpm.event_registered then + dt.register_event( + MODULE_NAME, "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + cpm.event_registered = true + end +end + +local script_data = {} + +script_data.metadata = { + name = _("color profile manager"), + purpose = _("manage external darktable color profiles"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/color_profile_manager" +} + +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = restart + +return script_data \ No newline at end of file diff --git a/contrib/copy_attach_detach_tags.lua b/contrib/copy_attach_detach_tags.lua index d7dab8ab..3f1f6d34 100644 --- a/contrib/copy_attach_detach_tags.lua +++ b/contrib/copy_attach_detach_tags.lua @@ -37,18 +37,38 @@ USAGE ]] local dt = require "darktable" +local du = require "lib/dtutils" local debug = require "darktable.debug" -local gettext = dt.gettext -dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0}) +local gettext = dt.gettext.gettext --- 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/") +du.check_min_api_version("7.0.0", "copy_attach_detach_tags") local function _(msgid) - return gettext.dgettext("copy_attach_detach_tags", 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 + +local cadt = {} +cadt.module_installed = false +cadt.event_registered = false +cadt.widget_table = {} + local image_tags = {} @@ -83,7 +103,7 @@ local function mcopy_tags() end end - dt.print(_('Image tags copied ...')) + dt.print(_('image tags copied ...')) --create UI tag list local taglist = "" @@ -105,7 +125,7 @@ local function mcopy_tags() local function attach_tags() if next(image_tags) == nil then - dt.print(_('No tags to attached, please copy tags first.')) + dt.print(_('no tag to attach, please copy tags first.')) return true end @@ -129,7 +149,7 @@ local function attach_tags() end end end - dt.print(_('Tags attached ...')) + dt.print(_('tags attached ...')) end local function detach_tags() @@ -145,13 +165,68 @@ 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() + if not cadt.module_installed then + dt.register_lib("tagging_addon",_('tagging addon'),true,true,{ + [dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER",500} + }, + dt.new_widget("box") { + -- orientation = "vertical", + reset_callback = function() + taglist_label.label = "" + image_tags = {} + end, + table.unpack(cadt.widget_table), + }, + nil, + nil + ) + cadt.module_installed = true + end +end + +local function destroy() + dt.destroy_event("cadt_ct", "shortcut") + dt.destroy_event("cadt_at", "shortcut") + dt.destroy_event("cadt_dt", "shortcut") + dt.destroy_event("cadt_rt", "shortcut") + dt.gui.libs["tagging_addon"].visible = false +end + +local function restart() + -- shortcut for copy + dt.register_event("cadt_ct", "shortcut", + mcopy_tags, + _('copy tags from selected image(s)')) + + -- shortcut for attach + dt.register_event("cadt_at", "shortcut", + attach_tags, + _('paste tags to selected image(s)')) + + -- shortcut for detaching tags + dt.register_event("cadt_dt", "shortcut", + detach_tags, + _('remove tags from selected image(s)')) + + -- shortcut for replace tags + dt.register_event("cadt_rt", "shortcut", + replace_tags, + _('replace tags from selected image(s)')) + dt.gui.libs["tagging_addon"].visible = true +end + +local function show() + dt.gui.libs["tagging_addon"].visible = true end -- create modul Tagging addons @@ -167,73 +242,82 @@ local taglabel = dt.new_widget("label") { local box1 = dt.new_widget("box"){ orientation = "horizontal", dt.new_widget("button") { - label = _('multi copy tags'), - clicked_callback = mcopy_tags}, + label = _('multi copy tags'), + tooltip = _('copy tags from selected image(s)'), + clicked_callback = mcopy_tags}, dt.new_widget("button") { - label = _('paste tags'), - clicked_callback = attach_tags} - + tooltip = _('paste tags to selected image(s)'), + label = _('paste tags'), + clicked_callback = attach_tags} } local box2 = dt.new_widget("box"){ orientation = "horizontal", dt.new_widget("button") { - label = _('replace tags'), - clicked_callback = replace_tags}, + label = _('replace tags'), + tooltip = _('replace tags from selected image(s)'), + clicked_callback = replace_tags}, dt.new_widget("button") { - label = _('remove all tags'), - clicked_callback = detach_tags} + label = _('remove all tags'), + tooltip = _('remove tags from selected image(s)'), + clicked_callback = detach_tags} } local sep = dt.new_widget("separator"){} -- pack elements into widget table for a nicer layout -local widget_table = {} -widget_table[1] = box1 -widget_table[#widget_table+1] = box2 +cadt.widget_table[1] = box1 +cadt.widget_table[#cadt.widget_table+1] = box2 -widget_table[#widget_table+1] = sep -widget_table[#widget_table+1] = taglabel -widget_table[#widget_table+1] = taglist_label +cadt.widget_table[#cadt.widget_table+1] = sep +cadt.widget_table[#cadt.widget_table+1] = taglabel +cadt.widget_table[#cadt.widget_table+1] = taglist_label -- create modul -dt.register_lib("tagging_addon","Tagging addon",true,true,{ - [dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER",500} - }, - dt.new_widget("box") { - -- orientation = "vertical", - reset_callback = function() - taglist_label.label = "" - image_tags = {} - end, - table.unpack(widget_table), - }, - nil, - nil - ) +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not cadt.event_registered then + dt.register_event( + "cadt", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + cadt.event_registered = true + end +end -- shortcut for copy -dt.register_event("shortcut", +dt.register_event("cadt_ct", "shortcut", mcopy_tags, _('copy tags from selected image(s)')) -- shortcut for attach -dt.register_event("shortcut", +dt.register_event("cadt_at", "shortcut", attach_tags, _('paste tags to selected image(s)')) -- shortcut for detaching tags -dt.register_event("shortcut", +dt.register_event("cadt_dt", "shortcut", detach_tags, _('remove tags from selected image(s)')) -- shortcut for replace tags -dt.register_event("shortcut", +dt.register_event("cadt_rt", "shortcut", replace_tags, _('replace tags from selected image(s)')) +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = show + +return script_data -- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua -- kate: tab-indents: off; indent-width 2; replace-tabs on; remove-trailing-space on; diff --git a/contrib/cr2hdr.lua b/contrib/cr2hdr.lua index 3d14fb16..36f325f2 100644 --- a/contrib/cr2hdr.lua +++ b/contrib/cr2hdr.lua @@ -33,9 +33,31 @@ USAGE ]] local darktable = require "darktable" +local du = require "lib/dtutils" --- Tested with darktable 2.0.1 -darktable.configuration.check_version(...,{2,0,0},{3,0,0},{4,0,0}) +du.check_min_api_version("7.0.0", "cr2hdr") + +local gettext = darktable.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("cr2hdr"), + purpose = _("process Magic Lantern dual ISO images"), + author = "Till Theato ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/cr2hdr" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them local queue = {} local processed_files = {} @@ -60,7 +82,7 @@ end local function convert_image(image) if string.sub(image.filename, -3) == "CR2" then local filename = image.path .. "/" .. image.filename - local result = dt.control.execute( "cr2hdr " .. filename) + local result = darktable.control.execute( "cr2hdr " .. filename) local out_filename = string.gsub(filename, ".CR2", ".DNG") local file = io.open(out_filename) if file then @@ -78,7 +100,7 @@ end local function convert_images() if next(queue) == nil then return end - job = darktable.gui.create_job("Dual ISO conversion", true, stop_conversion) + job = darktable.gui.create_job(_("dual ISO conversion"), true, stop_conversion) for key,image in pairs(queue) do if job.valid then job.percent = (key-1)/#queue @@ -89,7 +111,7 @@ local function convert_images() end local success_count = 0 for _ in pairs(processed_files) do success_count = success_count + 1 end - darktable.print("Dual ISO conversion successful on " .. success_count .. "/" .. #queue .. " images.") + darktable.print(string.format(_("dual ISO conversion successful on %d/%d images."), success_count, #queue)) job.valid = false processed_files = {} queue = {} @@ -106,8 +128,21 @@ local function convert_action_images(shortcut) convert_images() end -darktable.register_event("shortcut", convert_action_images, "Run cr2hdr (Magic Lantern DualISO converter) on selected images") -darktable.register_event("post-import-image", file_imported) -darktable.register_event("post-import-film", film_imported) +local function destroy() + darktable.destroy_event("cr2hdr", "shortcut") + darktable.destroy_event("cr2hdr", "post-import-image") + darktable.destroy_event("cr2hdr", "post-import-film") +end + +darktable.register_event("cr2hdr", "shortcut", + 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) + +script_data.destroy = destroy -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) +return script_data diff --git a/contrib/cycle_group_leader.lua b/contrib/cycle_group_leader.lua new file mode 100644 index 00000000..e168e93b --- /dev/null +++ b/contrib/cycle_group_leader.lua @@ -0,0 +1,162 @@ +--[[ + + cycle_group_leader.lua - change image group leader + + Copyright (C) 2024 Bill Ferguson + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +]] +--[[ + cycle_group_leader - change image grouip leader + + cycle_group_leader changes the group leader to the next + image in the group. If the end of the group is reached + then the next image is wrapped around to the first image. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + None + + USAGE + * enable with script_manager + * assign a key to the shortcut + + BUGS, COMMENTS, SUGGESTIONS + Bill Ferguson + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "cycle_group_leader" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("7.0.0", MODULE) + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- I 1 8 N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- S C R I P T M A N A G E R I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local script_data = {} + +script_data.metadata = { + name = _("cycle group leader"), + purpose = _("change image group leader"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/cycle_group_leader" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function toggle_global_toolbox_grouping() + dt.gui.libs.global_toolbox.grouping = false + dt.gui.libs.global_toolbox.grouping = true +end + +local function hinter_msg(msg) + dt.print_hinter(msg) + dt.control.sleep(1500) + dt.print_hinter(" ") +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N P R O G R A M +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function cycle_group_leader(image) + local group_images = image:get_group_members() + if #group_images < 2 then + hinter_msg(_("no images to cycle through in group")) + return + else + local position = nil + for i, img in ipairs(group_images) do + if image == img then + position = i + end + end + + if position == #group_images then + position = 1 + else + position = position + 1 + end + + new_leader = group_images[position] + new_leader:make_group_leader() + dt.gui.selection({new_leader}) + + if dt.gui.libs.global_toolbox.grouping then + -- toggle the grouping to make the new leader show + toggle_global_toolbox_grouping() + end + end +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + -- put things to destroy (events, storages, etc) here + dt.destroy_event(MODULE, "shortcut") +end + +script_data.destroy = destroy + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.register_event(MODULE, "shortcut", + function(event, shortcut) + -- ignore the film roll, it contains all the images, not just the imported + local images = dt.gui.selection() + if #images < 1 then + dt.print(_("no image selected, please select an image and try again")) + else + cycle_group_leader(images[1]) + end + end, + _("cycle group leader") +) + +return script_data diff --git a/contrib/dbmaint.lua b/contrib/dbmaint.lua new file mode 100644 index 00000000..c08309a2 --- /dev/null +++ b/contrib/dbmaint.lua @@ -0,0 +1,334 @@ +--[[ + + dbmaint.lua - perform database maintenance + + Copyright (C) 2024 Bill Ferguson . + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +]] +--[[ + dbmaint - perform database maintenance + + Perform database maintenance to clean up missing images and filmstrips. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + None + + USAGE + * start dbmaint from script_manager + * scan for missing film rolls or missing images + * look at the results and choose to delete or not + + BUGS, COMMENTS, SUGGESTIONS + Bill Ferguson + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local log = require "lib/dtutils.log" + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "dbmaint" +local DEFAULT_LOG_LEVEL = log.error +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("7.0.0", MODULE) -- choose the minimum version that contains the features you need + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- I 1 8 N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- S C R I P T M A N A G E R I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local script_data = {} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +script_data.metadata = { + name = _("db maintenance"), + purpose = _("perform database maintenance"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/dbmaint/" +} + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- L O G L E V E L +-- - - - - - - - - - - - - - - - - - - - - - - - + +log.log_level(DEFAULT_LOG_LEVEL) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local dbmaint = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- G L O B A L V A R I A B L E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dbmaint.main_widget = nil + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A L I A S E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local namespace = dbmaint + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +------------------- +-- helper functions +------------------- + +local function set_log_level(level) + local old_log_level = log.log_level() + log.log_level(level) + return old_log_level +end + +local function restore_log_level(level) + log.log_level(level) +end + +local function scan_film_rolls() + local missing_films = {} + + for _, filmroll in ipairs(dt.films) do + if not df.check_if_file_exists(filmroll.path) then + table.insert(missing_films, filmroll) + end + end + + return missing_films +end + +local function scan_images(film) + local old_log_level = set_log_level(DEFAULT_LOG_LEVEL) + local missing_images = {} + + if film then + for i = 1, #film do + local image = film[i] + log.msg(log.debug, "checking " .. image.filename) + if not df.check_if_file_exists(image.path .. PS .. image.filename) then + log.msg(log.info, image.filename .. " not found") + table.insert(missing_images, image) + end + end + end + + restore_log_level(old_log_level) + return missing_images +end + +local function remove_missing_film_rolls(list) + for _, filmroll in ipairs(list) do + filmroll:delete(true) + end +end + +-- force the lighttable to reload + +local function refresh_lighttable(film) + local rules = dt.gui.libs.collect.filter() + dt.gui.libs.collect.filter(rules) +end + +local function remove_missing_images(list) + local film = list[1].film + for _, image in ipairs(list) do + image:delete() + end + refresh_lighttable(film) +end + +local function install_module() + if not namespace.module_installed then + dt.register_lib( + MODULE, -- Module name + _("DB maintenance"), -- Visible name + true, -- expandable + true, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 0}}, -- containers + namespace.main_widget, + nil,-- view_enter + nil -- view_leave + ) + namespace.module_installed = true + end +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- U S E R I N T E R F A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +dbmaint.list_widget = dt.new_widget("text_view"){ + editable = false, + reset_callback = function(this) + this.text = "" + end +} + +dbmaint.chooser = dt.new_widget("combobox"){ + label = _("scan for"), + selected = 1, + _("film rolls"), _("images"), + reset_callback = function(this) + this.selected = 1 + end +} + +dbmaint.scan_button = dt.new_widget("button"){ + label = _("scan"), + tooltip = _("click to scan for missing film rolls/files"), + clicked_callback = function(this) + local found = nil + local found_text = "" + local old_log_level = set_log_level(DEFAULT_LOG_LEVEL) + log.msg(log.debug, "Button clicked") + if dbmaint.chooser.selected == 1 then -- film rolls + found = scan_film_rolls() + if #found > 0 then + for _, film in ipairs(found) do + local dir_name = du.split(film.path, PS) + found_text = found_text .. dir_name[#dir_name] .. "\n" + end + end + else + log.msg(log.debug, "checking path " .. dt.collection[1].path .. " for missing files") + found = scan_images(dt.collection[1].film) + if #found > 0 then + for _, image in ipairs(found) do + found_text = found_text .. image.filename .. "\n" + end + end + end + if #found > 0 then + log.msg(log.debug, "found " .. #found .. " missing items") + dbmaint.list_widget.text = found_text + dbmaint.found = found + dbmaint.remove_button.sensitive = true + else + log.msg(log.debug, "no missing items found") + end + restore_log_level(old_log_level) + end, + reset_callback = function(this) + dbmaint.found = nil + end +} + +dbmaint.remove_button = dt.new_widget("button"){ + label = _("remove"), + tooltip = _("remove missing film rolls/images"), + sensitive = false, + clicked_callback = function(this) + if dbmaint.chooser.selected == 1 then -- film rolls + remove_missing_film_rolls(dbmaint.found) + else + remove_missing_images(dbmaint.found) + end + dbmaint.found = nil + dbmaint.list_widget.text = "" + this.sensitive = false + end, + reset_callback = function(this) + this.sensitive = false + end +} + +dbmaint.main_widget = dt.new_widget("box"){ + orientation = "vertical", + dt.new_widget("section_label"){label = _("missing items")}, + dbmaint.list_widget, + dt.new_widget("label"){label = ""}, + dbmaint.chooser, + dt.new_widget("label"){label = ""}, + dbmaint.scan_button, + dbmaint.remove_button +} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + dt.gui.libs[MODULE].visible = false + + if namespace.event_registered then + dt.destroy_event(MODULE, "view-changed") + end + + return +end + +local function restart() + dt.gui.libs[MODULE].visible = true + + return +end + +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = restart + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not namespace.event_registered then + dt.register_event(MODULE, "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + namespace.event_registered = true + end +end + +return script_data \ No newline at end of file diff --git a/contrib/enfuseAdvanced.lua b/contrib/enfuseAdvanced.lua new file mode 100644 index 00000000..5027c564 --- /dev/null +++ b/contrib/enfuseAdvanced.lua @@ -0,0 +1,1168 @@ +--[[Enfuse Advanced plugin for darktable 2.2.X and 2.4.X + + copyright (c) 2017, 2018 Holger Klemm (Original Linux-only version) + Modified by: Kevin Ertel + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +]] + +--[[About this plugin +This plugin will add the new export module 'fusion to DRI or DFF image'. + +----REQUIRED SOFTWARE---- +align_image_stack +enfuse ver. 4.2 or greater +exiftool + +----USAGE---- +Install: + 1) Get the Lua scripts: https://github.com/darktable-org/lua-scripts#download-and-install + 2) Require this file in your luarc file, as with any other dt plug-in: require "contrib/enfuseAdvanced" + 3) Then select "DRI or DFF image" as storage option + 4) On the initial startup set your executable paths + +DRI = Dynamic Range Increase (Blend multiple bracket images into a single LDR file) +DFF = Depth From Focus ('Focus Stacking' - Blend multiple images with different focus into a single image) +Select multiple images that are either bracketed, or focus-shifted, set your desired operating parameters, and press the export button. A new image will be created. The image will +be auto imported into darktable if you have that option enabled. Additional tags or style can be applied on auto import as well, if you desire. + +image align options: +See align_image_stack documentation for further explanation of how it specifically works and the options provided (http://hugin.sourceforge.net/docs/manual/Align_image_stack.html) + +image fustion options: +See enfuse documentation for further explanation of how it specifically works and the options provided (https://wiki.panotools.org/Enfuse) +If you have a specific set of parameters you frequently like to use, you can save them to a preset. There are 3 presets available for DRI, and 3 for DFF. + +target file: +Select your file destination path, or check the 'save to source image location' option. +'Create Unique Filename' is enabled by default at startup, the user can choose to overwrite existing +Set any tags or style you desire to be added to the new image (only available if the auto-import option is enabled). You can also change the defaults for this under settings > lua options + +format options: +Same as other export modules + +global options: +Same as other export modules + +]] + +local dt = require 'darktable' +local df = require 'lib/dtutils.file' +local dsys = require 'lib/dtutils.system' +local du = require 'lib/dtutils' +local mod = 'module_enfuseAdvanced' +local os_path_seperator = '/' +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 + +-- INITS -- +local AIS = { + name = 'align_image_stack', + bin = '', + first_run = true, + install_error = false, + arg_string = '', + images_string = '', + args = { + radial_distortion = {text = '-d', style = 'bool'}, + optimize_field = {text = '-m', style = 'bool'}, + optimize_image_center = {text = '-i', style = 'bool'}, + auto_crop = {text = '-C', style = 'bool'}, + distortion = {text = '--distortion', style = 'bool'}, + gpu = {text = '--gpu', style = 'bool'}, + grid_size = {text = '-g ', style = 'integer'}, + control_points = {text = '-c ', style = 'integer'}, + control_points_remove = {text = '-t ', style = 'integer'}, + correlation = {text = '--corr=', style = 'float'} + } +} +local ENF = { + name = 'enfuse', + bin = '', + first_run = true, + install_error = false, + arg_string = '', + image_string = '', + args = { + exposure_weight = {text = '--exposure-weight=', style = 'float'}, + saturation_weight = {text = '--saturation-weight=', style = 'float'}, + contrast_weight = {text = '--contrast-weight=', style = 'float'}, + exposure_optimum = {text = '--exposure-optimum=', style = 'float'}, + exposure_width = {text = '--exposure-width=', style = 'float'}, + hard_masks = {text = '--hard-mask', style = 'bool'}, + save_masks = {text = '--save-masks', style = 'bool'}, + contrast_window_size = {text = '--contrast-window-size=', style = 'integer'}, + contrast_edge_scale = {text = '--contrast-edge-scale=', style = 'float'}, + contrast_min_curvature = {text = '--contrast-min-curvature=', style = 'string'} + } +} +local EXF = { + name = 'exiftool', + bin = '', + first_run = true, + install_error = false +} +local GUI = { + AIS = { + radial_distortion = {}, + optimize_field = {}, + optimize_image_center = {}, + auto_crop = {}, + distortion = {}, + grid_size = {}, + control_points = {}, + control_points_remove = {}, + correlation = {}}, + ENF = { + exposure_weight = {}, + saturation_weight = {}, + contrast_weight = {}, + exposure_optimum = {}, + exposure_width = {}, + hard_masks = {}, + save_masks = {}, + contrast_window_size = {}, + contrast_edge_scale = {}, + contrast_min_curvature = {}}, + Target = { + format = {}, + depth = {}, + compression_level_tif = {}, + compression_level_jpg = {}, + output_compress = {}, + output_directory = {}, + source_location = {}, + on_conflict = {}, + auto_import = {}, + apply_style = {}, + copy_tags = {}, + add_tags = {}}, + Presets = { + current_preset ={}; + load ={}; + save ={}; + variants ={}; + variants_type ={}}, + exes = { + align_image_stack = {}, + enfuse = {}, + exiftool = {}, + update = {} + }, + align = {}, + options_contain = {}, + show_options = {} +} + +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 + +-- FUNCTION -- + +local function sanitize_decimals(cmd) -- make sure decimal separator is a '.' + return string.gsub(cmd, '(%d),(%d)', "%1.%2") +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_tbls) --looks to see if this is the first call, if so checks to see if program is installed properly + for _,prog in pairs(prog_tbls) 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.options_contain.active = 4 + GUI.show_options.sensitive = false + dt.print(_('please update your binary locations')) + end +end + +local function ExeUpdate(prog_tbl) --updates executable paths and verifies them + dt.preferences.write(mod, 'bin_exists', 'bool', true) + for x,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) + dt.print(string.format(_("issue with %s executable"), prog.name)) + else + prog.bin = CleanSpaces(prog.bin) + end + prog.first_run = false + end + if dt.preferences.read(mod, 'bin_exists', 'bool') then + GUI.options_contain.active = 2 + GUI.show_options.sensitive = true + dt.print(_('update successful')) + else + dt.print(_('update unsuccessful, please try again')) + end +end + +local function GetArgsFromPreference(prog_table, prefix) --for each arg in a program table reads in the associated value in the active preference (which is continually updated via clicked/changed callback in GUI elements + prog_table.arg_string = '' + for argument, arg_data in pairs(prog_table.args) do + 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' 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) + prog_table.arg_string = prog_table.arg_string..arg_data.text..temp..' ' + end + end + prog_table.arg_string = CleanSpaces(prog_table.arg_string) + return prog_table.arg_string +end + +local function UpdateAISargs(image_table, images_to_remove) --updates the AIS arguments, builds the input image string, returns a modified image table which contains the aligned image names in place of the exported image names also updates the images to remove string with new aligned images and a string of the images to align + GetArgsFromPreference(AIS, 'active_') + local source_path = '' + local images_to_align = '' + local index = 0 + for raw, temp_image in pairs(image_table) do + local index_str = '' + source_path = GetFileName(temp_image) + images_to_align = images_to_align..df.sanitize_filename(temp_image)..' ' + if InRange(index,0,9) then index_str = '000'..tostring(index) + elseif InRange(index,10,99) then index_str = '00'..tostring(index) + end + image_table[raw] = source_path..'aligned_'..index_str..'.tif' + images_to_remove = images_to_remove..df.sanitize_filename(image_table[raw])..' ' + index = index + 1 + end + source_path = df.sanitize_filename(source_path..'aligned_') + AIS.arg_string = AIS.arg_string..' -a '..source_path + images_to_align = CleanSpaces(images_to_align) + + return image_table, images_to_remove, images_to_align +end + +local function UpdateENFargs(image_table, prefix) --updates the Enfuse arguments, builds the input image string, generates output filename, returns string of images to blend, the name of the outout image, and the first raw image object + GetArgsFromPreference(ENF, prefix) + local images_to_blend = '' + local out_path = '' + local out_name = '' + local smallest_name = '' + local smallest_id = math.huge + local largest_id = 0 + local first_raw = {} + for raw, temp_image in pairs(image_table) do + local _, source_name, source_id = GetFileName(raw.filename) + source_id = tonumber(source_id) + if source_id < smallest_id then + smallest_id = source_id + smallest_name = source_name + first_raw = raw + end + if source_id > largest_id then largest_id = source_id end + out_path = raw.path + images_to_blend = images_to_blend..df.sanitize_filename(temp_image)..' ' + 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='..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 + out_path = out_path..os_path_seperator..out_name..'.'..GUI.Target.format.value + if GUI.Target.on_conflict.value == 'create unique filename' then out_path = df.create_unique_filename(out_path) end + ENF.arg_string = ENF.arg_string..'--output='..df.sanitize_filename(out_path) + images_to_blend = CleanSpaces(images_to_blend) + + return images_to_blend, out_path, first_raw +end + +local function UpdateActivePreference() --sliders & entry boxes do not have a click/changed callback, so their values must be saved to the active preference 'manually' + local enf = {'exposure_weight','saturation_weight','contrast_weight','exposure_optimum','exposure_width'} + for _,descriptor in pairs(enf) do + temp = GUI.ENF[descriptor].value + dt.preferences.write(mod, 'active_'..descriptor, 'float', temp) + end + temp = GUI.Target.compression_level_jpg.value + temp = math.floor(temp) + dt.preferences.write(mod, 'active_compression_level_jpg', 'integer', temp) + temp = GUI.Target.add_tags.text + dt.preferences.write(mod, 'active_add_tags', 'string', temp) +end + +local function SaveToPreference(preset) --save the present values of enfuse GUI elements to the specified 'preset' + UpdateActivePreference() + for argument, arg_data in pairs(ENF.args) do + local temp + if argument == 'contrast_window_size' or argument == 'contrast_edge_scale' or argument == 'contrast_min_curvature' then --comboboxes must be handled specially via an index value + temp = dt.preferences.read(mod, 'active_'..argument..'_ind', 'integer') + dt.preferences.write(mod, preset..argument..'_ind', 'integer', temp) + temp = dt.preferences.read(mod, 'active_'..argument, arg_data.style) + dt.preferences.write(mod, preset..argument, arg_data.style, temp) + else + temp = dt.preferences.read(mod, 'active_'..argument, arg_data.style) + dt.preferences.write(mod, preset..argument, arg_data.style, temp) + end + end + dt.print(string.format(_("saved to %s"), preset)) +end + +local function LoadFromPreference(preset) --load values from the specified 'preset' into the GUI elements + for argument, arg_data in pairs(ENF.args) do + local temp + if argument == 'contrast_window_size' or argument == 'contrast_edge_scale' or argument == 'contrast_min_curvature' then --comboboxes must be handled specially via an index value + temp = dt.preferences.read(mod, preset..argument..'_ind', 'integer') + GUI.ENF[argument].selected = temp + dt.preferences.write(mod, 'active_'..argument..'_ind', 'integer', temp) + else + temp = dt.preferences.read(mod, preset..argument, arg_data.style) + GUI.ENF[argument].value = temp + dt.preferences.write(mod, 'active_'..argument, arg_data.style, temp) + end + end + dt.print(string.format(_("loaded from %s"), preset)) +end + +local function remove_temp_files(images_to_remove) --deletes all files specified by the input string + if dt.configuration.running_os == 'windows' then + dt.control.execute('del '..images_to_remove) + else + dt.control.execute('rm '..images_to_remove) + end +end + +local function initial(storage, format, images, high_quality, extra_data) --called before export happens, ensure enough images selected and all necesary programs installed correctly, if error then it sets a bit in extra_data for use by main and cancels export + if #images <2 then + table.insert(extra_data, 1, 1) + return {} + else + PreCall({ENF,AIS,EXF}) + if AIS.install_error or ENF.install_error or EXF.install_error then + table.insert(extra_data, 1, 2) + return {} + else + table.insert(extra_data, 1, 0) + return images + end + end +end + +local function support_format(storage, format) --tells dt we only support TIFF export type + local ret = false + local temp = string.find(format.name, 'TIFF') + if temp ~= nil then ret = true end + return ret +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))) +end + +local function main(storage, image_table, extra_data) + if extra_data[1] == 1 then + 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')) + return + end + local images_to_remove = '' + local final_image = nil + local source_raw = nil + for raw,exported in pairs(image_table) do --add the exported files to list of images to remove when complete\fail + images_to_remove = images_to_remove..df.sanitize_filename(exported)..' ' + end + if GUI.align.value then --if user selected to align images then update AIS arguments and execute AIS command + local job = dt.gui.create_job('aligning images') + image_table, images_to_remove, AIS.images_string = UpdateAISargs(image_table, images_to_remove) + local run_cmd = BuildExecuteCmd(AIS) + dt.print_log("AIS run command is " .. run_cmd) + run_cmd = sanitize_decimals(run_cmd) + dt.print_log("AIS decimaal sanitized command is " .. run_cmd) + local resp = dsys.external_command(run_cmd) + 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') + return + end + end + variants = {'active_'} --default to 'active' settings unless user selected to create multiple image variants from the specified preset types + if GUI.Presets.variants.value then + if GUI.Presets.variants_type.value == 'dri' then + variants = {'dri_1', 'dri_2', 'dri_3'} + else + variants = {'dff_2', 'dff_2', 'dff_3'} + end + end + local image_num = 0 + local job = dt.gui.create_job('blending '..#variants..' image(s)', true) --create a GUI job bar to display enfuse progress + UpdateActivePreference() --load current GUI values into active preference (only applies to elements without a clicked/changed callback) + for x,prefix in pairs(variants) do --for each image to be created load in the preference values, build arguments string, output image, and run command then execute. + job.percent = image_num /(#variants) + image_num = image_num+1 + ENF.images_string, final_image, source_raw = UpdateENFargs(image_table, prefix) + local run_cmd = BuildExecuteCmd(ENF) + dt.print_log("ENF run command is " .. run_cmd) + run_cmd = sanitize_decimals(run_cmd) + dt.print_log("ENF decimaal sanitized command is " .. run_cmd) + 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)) + return + end + + --copy exif data from original file + run_cmd = EXF.bin..' -TagsFromFile '..df.sanitize_filename(source_raw.path..os_path_seperator..source_raw.filename)..' -exif:all --subifd:all -overwrite_original '..df.sanitize_filename(final_image) + -- replace comma decimal separator with period + dt.print_log("EXF run command is " .. run_cmd) + run_cmd = sanitize_decimals(run_cmd) + dt.print_log("EXF decimaal sanitized command is " .. run_cmd) + resp = dsys.external_command(run_cmd) + + + if GUI.Target.auto_import.value then --import image into dt if specified + local imported = dt.database.import(final_image) + dt.print_log("image imported") + if GUI.Target.apply_style.selected > 1 then --apply specified style to imported image + local set_style = styles[GUI.Target.apply_style.selected - 1] + dt.styles.apply(set_style , imported) + end + if GUI.Target.copy_tags.value then --copy tags from source image + 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 + end + end + remove_temp_files(images_to_remove) + job.valid = false + dt.print('image fusion process complete') +end + +local function destroy() + dt.destroy_storage('module_enfuseAdvanced') +end + +--GUI-- +stack_compression = dt.new_widget('stack'){} +local label_AIS_options= dt.new_widget('section_label'){ + label = _('image align options') +} +GUI.align = dt.new_widget('check_button'){ + label = _('align images'), + value = dt.preferences.read(mod, 'active_align', 'bool'), + tooltip = _('automatically align images prior to enfuse'), + clicked_callback = function(self) + dt.preferences.write(mod, 'active_align', 'bool', self.value) + for _,widget in pairs(GUI.AIS) do + widget.sensitive = self.value + end + end, + reset_callback = function(self) self.value = true end +} +GUI.AIS.radial_distortion = dt.new_widget('check_button'){ + label = _('optimize radial distortion'), + value = dt.preferences.read(mod, 'active_radial_distortion', 'bool'), + tooltip = _('optimize radial distortion for all images, \nexcept for first'), + clicked_callback = function(self) dt.preferences.write(mod, 'active_radial_distortion', 'bool', self.value) end, + reset_callback = function(self) self.value = true end +} +GUI.AIS.optimize_field = dt.new_widget('check_button'){ + label = _('optimize field of view'), + value = dt.preferences.read(mod, 'active_optimize_field', 'bool'), + tooltip = _('optimize field of view for all images, except for first. \nUseful for aligning focus stacks (DFF) with slightly \ndifferent magnification.'), + clicked_callback = function(self) dt.preferences.write(mod, 'active_optimize_field', 'bool', self.value) end, + reset_callback = function(self) self.value = true end +} +GUI.AIS.optimize_image_center = dt.new_widget('check_button'){ + label = _('optimize image center shift'), + value = dt.preferences.read(mod, 'active_optimize_image_center', 'bool'), + tooltip = _('optimize image center shift for all images, \nexcept for first.'), + clicked_callback = function(self) dt.preferences.write(mod, 'active_optimize_image_center', 'bool', self.value) end, + reset_callback = function(self) self.value = true end +} +GUI.AIS.auto_crop = dt.new_widget('check_button'){ + label = _('auto crop'), + value = dt.preferences.read(mod, 'active_auto_crop', 'bool'), + tooltip = _('auto crop the image to the area covered by all images.'), + clicked_callback = function(self) dt.preferences.write(mod, 'active_auto_crop', 'bool', self.value) end, + reset_callback = function(self) self.value = true end +} +GUI.AIS.distortion = dt.new_widget('check_button'){ + label = _('load distortion from lens database'), + value = dt.preferences.read(mod, 'active_distortion', 'bool'), + tooltip = _('try to load distortion information from lens database'), + clicked_callback = function(self) dt.preferences.write(mod, 'active_distortion', 'bool', self.value) end, + reset_callback = function(self) self.value = true end +} +GUI.AIS.gpu = dt.new_widget('check_button'){ + label = _('use gpu'), + value = dt.preferences.read(mod, 'active_gpu', 'bool'), + tooltip = _('use gpu during alignment'), + clicked_callback = function(self) dt.preferences.write(mod, 'active_gpu', 'bool', self.value) end, + reset_callback = function(self) self.value = true end +} +temp = dt.preferences.read(mod, 'active_grid_size_ind', 'integer') +if not InRange(temp, 1, 9) then temp = 5 end +GUI.AIS.grid_size = dt.new_widget('combobox'){ + label = _('image grid size'), + tooltip = _('break image into a rectangular grid \nand attempt to find num control points in each section.\ndefault: (5x5)'), + selected = temp, + '1','2','3','4','5','6','7','8','9', + changed_callback = function(self) + dt.preferences.write(mod, 'active_grid_size', 'integer', self.value) + dt.preferences.write(mod, 'active_grid_size_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 5 + dt.preferences.write(mod, 'active_grid_size', 'integer', self.value) + dt.preferences.write(mod, 'active_grid_size_ind', 'integer', self.selected) + end +} +temp = dt.preferences.read(mod, 'active_control_points_ind', 'integer') +if not InRange(temp, 1, 9) then temp = 8 end +GUI.AIS.control_points = dt.new_widget('combobox'){ + label = _('control points/grid'), + tooltip = _('number of control points (per grid, see option -g) \nto create between adjacent images \ndefault: (8).'), + selected = temp, + '1','2','3','4','5','6','7','8','9', + changed_callback = function(self) + dt.preferences.write(mod, 'active_control_points', 'integer', self.value) + dt.preferences.write(mod, 'active_control_points_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 8 + dt.preferences.write(mod, 'active_control_points', 'integer', self.value) + dt.preferences.write(mod, 'active_control_points_ind', 'integer', self.selected) + end +} +temp = dt.preferences.read(mod, 'active_control_points_remove_ind', 'integer') +if not InRange(temp, 1, 9) then temp = 3 end +GUI.AIS.control_points_remove = dt.new_widget('combobox'){ + label = _('remove control points with error'), + tooltip = _('remove all control points with an error higher \nthan num pixels \ndefault: (3)'), + selected = temp, + '1','2','3','4','5','6','7','8','9', + changed_callback = function(self) + dt.preferences.write(mod, 'active_control_points_remove', 'integer', self.value) + dt.preferences.write(mod, 'active_control_points_remove_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 3 + dt.preferences.write(mod, 'active_control_points_remove', 'integer', self.value) + dt.preferences.write(mod, 'active_control_points_remove_ind', 'integer', self.selected) + end +} +temp = dt.preferences.read(mod, 'active_correlation_ind', 'integer') +if not InRange(temp, 1, 10) then temp = 9 end +GUI.AIS.correlation = dt.new_widget('combobox'){ + label = _('correlation threshold for control points'), + tooltip = _('correlation threshold for identifying \ncontrol points \ndefault: (0.9).'), + selected = temp, + '0.1','0.2','0.3','0.4','0.5','0.6','0.7','0.8','0.9','1.0', + changed_callback = function(self) + dt.preferences.write(mod, 'active_correlation', 'float', self.value) + dt.preferences.write(mod, 'active_correlation_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 9 + dt.preferences.write(mod, 'active_correlation', 'float', self.value) + dt.preferences.write(mod, 'active_correlation_ind', 'integer', self.selected) + end +} +local label_ENF_options= dt.new_widget('section_label'){ + label = _('image fusion options') +} +temp = dt.preferences.read(mod, 'active_exposure_weight', 'float') +if not InRange(temp, 0, 1) then temp = 1 end +GUI.ENF.exposure_weight = dt.new_widget('slider'){ + label = _('exposure weight'), + tooltip = _('set the relative weight of the well-exposedness criterion \nas defined by the chosen exposure weight function. \nincreasing this weight relative to the others will\n make well-exposed pixels contribute more to\n the final output. \ndefault: (1.0)'), + hard_min = 0, + hard_max = 1, + value = temp, + reset_callback = function(self) + self.value = 1 + end +} +temp = dt.preferences.read(mod, 'active_saturation_weight', 'float') +if not InRange(temp, 0, 1) then temp = .2 end +GUI.ENF.saturation_weight = dt.new_widget('slider'){ + label = _('saturation weight'), + tooltip = _('set the relative weight of high-saturation pixels. \nincreasing this weight makes pixels with high \nsaturation contribute more to the final output. \ndefault: (0.2)'), + hard_min = 0, + hard_max = 1, + value = temp, + reset_callback = function(self) + self.value = 0.2 + end +} +temp = dt.preferences.read(mod, 'active_contrast_weight', 'float') +if not InRange(temp, 0, 1) then temp = 0 end +GUI.ENF.contrast_weight = dt.new_widget('slider'){ + label = _('contrast weight'), + tooltip = _('sets the relative weight of high local-contrast pixels. \ndefault: (0.0).'), + hard_min = 0, + hard_max = 1, + value = temp, + reset_callback = function(self) + self.value = 0 + end +} +temp = dt.preferences.read(mod, 'active_exposure_optimum', 'float') +if not InRange(temp, 0, 1) then temp = 0.5 end +GUI.ENF.exposure_optimum = dt.new_widget('slider'){ + label = _('exposure optimum'), + tooltip = _('determine at what normalized exposure value\n the optimum exposure of the input images\n is. this is, set the position of the maximum\n of the exposure weight curve. use this \noption to fine-tune exposure weighting. \ndefault: (0.5)'), + hard_min = 0, + hard_max = 1, + value = temp, + reset_callback = function(self) + self.value = 0.5 + end +} +temp = dt.preferences.read(mod, 'active_exposure_width', 'float') +if not InRange(temp, 0, 1) then temp = 0.2 end +GUI.ENF.exposure_width = dt.new_widget('slider'){ + label = _('exposure width'), + tooltip = _('set the characteristic width (FWHM) of the exposure \nweight function. low numbers give less weight to \npixels that are far from the user-defined \noptimum and vice versa. use this option to \nfine-tune exposure weighting. \ndefault: (0.2)'), + hard_min = 0, + hard_max = 1, + value = temp, + reset_callback = function(self) + self.value = 0.2 + end +} +GUI.ENF.hard_masks = dt.new_widget('check_button'){ + label = _('hard mask'), + value = dt.preferences.read(mod, 'active_hard_masks', 'bool'), + tooltip = _('force hard blend masks on the finest scale. this avoids \naveraging of fine details (only), at the expense \nof increasing the noise. this improves the \nsharpness of focus stacks considerably.\ndefault (soft mask)'), + clicked_callback = function(self) dt.preferences.write(mod, 'active_hard_masks', 'bool', self.value) end, + reset_callback = function(self) self.value = false end +} +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.'), + clicked_callback = function(self) dt.preferences.write(mod, 'active_save_masks', 'bool', self.value) end, + reset_callback = function(self) self.value = false end +} +temp = dt.preferences.read(mod, 'active_contrast_window_size_ind', 'integer') +if not InRange(temp, 1, 8) then temp = 3 end +GUI.ENF.contrast_window_size = dt.new_widget('combobox'){ + label = _('contrast window size'), + tooltip = _('set the window size for local contrast analysis. \nthe window will be a square of size × size pixels. \nif given an even size, Enfuse will \nautomatically use the next odd number.\nfor contrast analysis size values larger \nthan 5 pixels might result in a \nblurry composite image. values of 3 and \n5 pixels have given good results on \nfocus stacks. \ndefault: (5) pixels'), + selected = temp, + '3','4','5','6','7','8','9','10', + changed_callback = function(self) + dt.preferences.write(mod, 'active_contrast_window_size', 'integer', self.value) + dt.preferences.write(mod, 'active_contrast_window_size_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 3 + dt.preferences.write(mod, 'active_contrast_window_size', 'integer', self.value) + dt.preferences.write(mod, 'active_contrast_window_size_ind', 'integer', self.selected) + end +} +temp = dt.preferences.read(mod, 'active_contrast_edge_scale_ind', 'integer') +if not InRange(temp, 1, 6) then temp = 1 end +GUI.ENF.contrast_edge_scale = dt.new_widget('combobox'){ + label = _('contrast edge scale'), + tooltip = _('a non-zero value for EDGE-SCALE switches on the \nLaplacian-of-Gaussian (LoG) edge detection algorithm.\n edage-scale is the radius of the Gaussian used \nin the search for edges. a positive LCE-SCALE \nturns on local contrast enhancement (LCE) \nbefore the LoG edge detection. \nDefault: (0.0) pixels.'), + selected = temp, + '0.0','0.1','0.2','0.3','0.4','0.5', + changed_callback = function(self) + dt.preferences.write(mod, 'active_contrast_edge_scale', 'float', self.value) + dt.preferences.write(mod, 'active_contrast_edge_scale_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 1 + dt.preferences.write(mod, 'active_contrast_edge_scale', 'float', self.value) + dt.preferences.write(mod, 'active_contrast_edge_scale_ind', 'integer', self.selected) + end +} +temp = dt.preferences.read(mod, 'active_contrast_min_curvature_ind', 'integer') +if not InRange(temp, 1, 11) then temp = 1 end +GUI.ENF.contrast_min_curvature = dt.new_widget('combobox'){ + label = _('contrast min curvature [%]'), + tooltip = _('define the minimum curvature for the LoG edge detection. Append a ‘%’ to specify the minimum curvature relative to maximum pixel value in the source image. Default: (0.0%)'), + selected = temp, + '0.0%','0.1%','0.2%','0.3%','0.4%','0.5%','0.6%','0.7%','0.8%','0.9%','1.0%', + changed_callback = function(self) + dt.preferences.write(mod, 'active_contrast_min_curvature', 'string', self.value) + dt.preferences.write(mod, 'active_contrast_min_curvature_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 1 + dt.preferences.write(mod, 'active_contrast_min_curvature', 'string', self.value) + dt.preferences.write(mod, 'active_contrast_min_curvature_ind', 'integer', self.selected) + end +} +local label_target_options= dt.new_widget('section_label'){ + label = _('target file') +} +temp = dt.preferences.read(mod, 'active_compression_level_tif_ind', 'integer') +if not InRange(temp, 1, 4) then temp = 1 end +GUI.Target.compression_level_tif = dt.new_widget('combobox'){ + label = _('tiff compression'), + tooltip = _('compression method for tiff files'), + selected = temp, + 'none','deflate','lzw','packbits', + changed_callback = function(self) + dt.preferences.write(mod, 'active_compression_level_tif', 'string', self.value) + dt.preferences.write(mod, 'active_compression_level_tif_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 1 + dt.preferences.write(mod, 'active_compression_level_tif', 'string', self.value) + dt.preferences.write(mod, 'active_compression_level_tif_ind', 'integer', self.selected) + end +} +temp = dt.preferences.read(mod, 'active_compression_level_jpg', 'integer') +if not InRange(temp, 0, 100) then temp = 0 end +GUI.Target.compression_level_jpg = dt.new_widget('slider'){ + label = _('jpeg compression'), + tooltip = _('jpeg compression level'), + soft_min = 0, + soft_max = 100, + hard_min = 0, + hard_max = 100, + step = 5, + digits = 0, + value = temp, + reset_callback = function(self) + self.value = 0 + end +} +local blank = dt.new_widget('box'){} +stack_compression = dt.new_widget('stack'){ + GUI.Target.compression_level_tif, + GUI.Target.compression_level_jpg, + blank, + active = 1 +} +temp = dt.preferences.read(mod, 'active_format_ind', 'integer') +if not InRange(temp, 1, 6) then temp = 1 end +GUI.Target.format = dt.new_widget('combobox'){ + label = _('file format'), + tooltip = _('file format of the enfused final image'), + selected = temp, + 'tif','jpg','png','pnm','pbm','ppm', + changed_callback = function(self) + dt.preferences.write(mod, 'active_format', 'string', self.value) + dt.preferences.write(mod, 'active_format_ind', 'integer', self.selected) + if self.value == 'tif' then stack_compression.active = 1 + elseif self.value == 'jpg' then stack_compression.active = 2 + else stack_compression.active = 3 + end + end, + reset_callback = function(self) + self.selected = 1 + dt.preferences.write(mod, 'active_format', 'string', self.value) + dt.preferences.write(mod, 'active_format_ind', 'integer', self.selected) + end +} +temp = dt.preferences.read(mod, 'active_depth_ind', 'integer') +if not InRange(temp, 1, 5) then temp = 3 end +GUI.Target.depth = dt.new_widget('combobox'){ + label = _('bit depth'), + tooltip = _('bit depth of the enfused file'), + selected = temp, + '8','16','32','r32','r64', + changed_callback = function(self) + dt.preferences.write(mod, 'active_depth', 'string', self.value) + dt.preferences.write(mod, 'active_depth_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 2 + dt.preferences.write(mod, 'active_depth', 'string', self.value) + dt.preferences.write(mod, 'active_depth_ind', 'integer', self.selected) + end +} +local label_directory = dt.new_widget('label'){ + label = _('directory'), + ellipsize = 'start', + halign = 'start' +} +temp = dt.preferences.read(mod, 'active_output_directory', 'string') +if temp == '' or temp == nil then temp = dt.collection[1].path end +GUI.Target.output_directory = dt.new_widget('file_chooser_button'){ + title = 'Select export path', + is_directory = true, + tooltip = _('select the target directory for the fused image. \nthe filename is created automatically.'), + value = temp, + changed_callback = function(self) dt.preferences.write(mod, 'active_output_directory', 'string', self.value) end +} +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.'), + clicked_callback = function(self) dt.preferences.write(mod, 'active_source_location', 'bool', self.value) end, + reset_callback = function(self) self.value = true end +} +temp = dt.preferences.read(mod, 'active_on_conflict_ind', 'integer') +if not InRange(temp, 1, 2) then temp = 1 end +GUI.Target.on_conflict = dt.new_widget('combobox'){ + label = _('on conflict'), + selected = 1, + 'create unique filename','overwrite', + changed_callback = function(self) + dt.preferences.write(mod, 'active_on_conflict', 'string', self.value) + dt.preferences.write(mod, 'active_on_conflict_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 1 + dt.preferences.write(mod, 'active_on_conflict', 'string', self.value) + dt.preferences.write(mod, 'active_on_conflict_ind', 'integer', self.selected) + end +} +GUI.Target.auto_import = dt.new_widget('check_button'){ + label = _('auto import'), + value = dt.preferences.read(mod, 'active_auto_import', 'bool'), + tooltip = _('import the image into darktable database when enfuse completes'), + clicked_callback = function(self) + dt.preferences.write(mod, 'active_auto_import', 'bool', self.value) + GUI.Target.apply_style.sensitive = self.value + GUI.Target.copy_tags.sensitive = self.value + GUI.Target.add_tags.sensitive = self.value + end, + reset_callback = function(self) self.value = true end +} +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'), + selected = 1, + 'none', + changed_callback = function(self) + dt.preferences.write(mod, 'active_apply_style', 'string', self.value) + dt.preferences.write(mod, 'active_apply_style_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 1 + dt.preferences.write(mod, 'active_apply_style', 'string', self.value) + dt.preferences.write(mod, 'active_apply_style_ind', 'integer', self.selected) + end +} +for k=1, (styles_count-1) do + GUI.Target.apply_style[k+1] = styles[k].name +end +if not InRange(temp, 1, styles_count) then temp = 1 end +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.'), + 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, separated by commas'), + editable = true +} +temp = dt.preferences.read(mod, 'active_current_preset_ind', 'integer') +if not InRange(temp, 1, 6) then temp = 1 end +GUI.Presets.current_preset = dt.new_widget('combobox'){ + label = _('active preset'), + tooltip = _('preset to be loaded from or saved to'), + value = temp, + 'dri_1', 'dri_2', 'dri_3', 'dff_1', 'dff_2', 'dff_3', + changed_callback = function(self) + dt.preferences.write(mod, 'active_current_preset', 'string', self.value) + dt.preferences.write(mod, 'active_current_preset_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 1 + dt.preferences.write(mod, 'active_current_preset', 'string', self.value) + dt.preferences.write(mod, 'active_current_preset_ind', 'integer', self.selected) + end +} +GUI.Presets.load = dt.new_widget('button'){ + label = _('load fusion preset'), + tooltip = _('load current fusion parameters from selected preset'), + clicked_callback = function() LoadFromPreference(GUI.Presets.current_preset.value) end +} +GUI.Presets.save = dt.new_widget('button'){ + label = _('save to fusion preset'), + tooltip = _('save current fusion parameters to selected preset'), + clicked_callback = function() SaveToPreference(GUI.Presets.current_preset.value) end +} +GUI.Presets.variants = dt.new_widget('check_button'){ + label = _('create image variants from presets'), + value = dt.preferences.read(mod, 'active_variants', 'bool'), + tooltip = _('create multiple image variants based on the three different presets of the specified type'), + clicked_callback = function(self) + dt.preferences.write(mod, 'active_variants', 'bool', self.value) + if self.value then + GUI.Target.on_conflict.selected = 1 + GUI.Target.on_conflict.sensitive = false + GUI.Presets.variants_type.sensitive = true + else + GUI.Target.on_conflict.sensitive = true + GUI.Presets.variants_type.sensitive = false + end + end, + reset_callback = function(self) self.value = false end +} +temp = dt.preferences.read(mod, 'active_variants_type_ind', 'integer') +if not InRange(temp, 1, 2) then temp = 1 end +GUI.Presets.variants_type = dt.new_widget('combobox'){ + label = _('create variants type'), + tooltip = _('preset type to be used when creating image variants'), + selected = temp, + 'dri', 'dff', + changed_callback = function(self) + dt.preferences.write(mod, 'active_variants_type', 'string', self.value) + dt.preferences.write(mod, 'active_variants_type_ind', 'integer', self.selected) + end, + reset_callback = function(self) + self.selected = 1 + dt.preferences.write(mod, 'active_variants_type', 'string', self.value) + dt.preferences.write(mod, 'active_variants_type_ind', 'integer', self.selected) + end +} +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'), + value = temp, + tooltip = temp, + is_directory = false, + changed_callback = function(self) self.tooltip = self.value end +} +temp = df.get_executable_path_preference(ENF.name) +GUI.exes.enfuse = dt.new_widget('file_chooser_button'){ + title = 'enfuse ' .. _('binary path'), + value = temp, + tooltip = temp, + is_directory = false, + changed_callback = function(self) self.tooltip = self.value end +} +temp = df.get_executable_path_preference(EXF.name) +GUI.exes.exiftool = dt.new_widget('file_chooser_button'){ + title = 'exiftool ' .. _('binary path'), + value = temp, + tooltip = temp, + is_directory = false, + changed_callback = function(self) self.tooltip = self.value end +} +GUI.exes.update = dt.new_widget('button'){ + label = _('update'), + tooltip = _('update the binary paths with current values'), + clicked_callback = function() ExeUpdate({AIS,ENF,EXF}) end +} +temp = GUI.Target.format.value +if temp == 'tif' then temp = 1 +elseif temp == 'jpg' then temp = 2 +else temp = 3 +end +stack_compression.active = temp +for _,widget in pairs(GUI.AIS) do + widget.sensitive = GUI.align.value +end +GUI.Target.apply_style.sensitive = GUI.Target.auto_import.value +GUI.Target.copy_tags.sensitive = GUI.Target.auto_import.value +GUI.Target.add_tags.sensitive = GUI.Target.auto_import.value + +local box_AIS = dt.new_widget('box'){ + orientation = 'vertical', + label_AIS_options, + GUI.align, + GUI.AIS.radial_distortion, + GUI.AIS.optimize_field, + GUI.AIS.optimize_image_center, + GUI.AIS.auto_crop, + GUI.AIS.distortion, + GUI.AIS.gpu, + GUI.AIS.grid_size, + GUI.AIS.control_points, + GUI.AIS.control_points_remove, + GUI.AIS.correlation +} +local box_ENF = dt.new_widget('box'){ + orientation = 'vertical', + label_ENF_options, + GUI.ENF.exposure_weight, + GUI.ENF.saturation_weight, + GUI.ENF.contrast_weight, + GUI.ENF.exposure_optimum, + GUI.ENF.exposure_width, + GUI.ENF.hard_masks, + GUI.ENF.save_masks, + GUI.ENF.contrast_window_size, + GUI.ENF.contrast_edge_scale, + GUI.ENF.contrast_min_curvature, + GUI.Presets.current_preset, + GUI.Presets.load, + GUI.Presets.save +} +local box_Target = dt.new_widget('box'){ + orientation = 'vertical', + label_target_options, + GUI.Target.format, + GUI.Target.depth, + stack_compression, + label_directory, + GUI.Target.output_directory, + GUI.Target.source_location, + GUI.Target.on_conflict, + GUI.Presets.variants, + GUI.Presets.variants_type, + GUI.Target.auto_import, + GUI.Target.apply_style, + GUI.Target.copy_tags, + GUI.Target.add_tags +} +local box_exes = dt.new_widget('box'){ + orientation = 'vertical', + GUI.exes.align_image_stack, + GUI.exes.enfuse, + GUI.exes.exiftool, + GUI.exes.update +} +GUI.options_contain = dt.new_widget('stack'){ + box_AIS, + box_ENF, + box_Target, + box_exes, + active = 2 +} +GUI.show_options = dt.new_widget('combobox'){ + label = _('show options'), + tooltip = _('show options for specified aspect of output'), + selected = 2, + 'align image stack', 'enfuse/enblend', 'target file', + changed_callback = function(self) + GUI.options_contain.active = self.selected + end, + reset_callback = function(self) + self.selected = 1 + dt.preferences.write(mod, 'active_current_preset', 'string', self.value) + dt.preferences.write(mod, 'active_current_preset_ind', 'integer', self.selected) + end +} +local storage_widget = dt.new_widget('box') { + orientation = 'vertical', + GUI.show_options, + GUI.options_contain +} + +-- Register new storage -- +dt.register_storage( + 'module_enfuseAdvanced', --Module name + _('DRI or DFF image'), --Name + show_status, --store: called once per exported image + main, --finalize: called once when all images have finished exporting + support_format, --supported + initial, --initialize + storage_widget +) + +if dt.preferences.read(mod, 'bin_exists', 'bool') then + GUI.options_contain.active = 2 + GUI.show_options.sensitive = true +else + GUI.options_contain.active = 4 + GUI.show_options.sensitive = false +end + +script_data.destroy = destroy + +return script_data \ No newline at end of file diff --git a/contrib/exportLUT.lua b/contrib/exportLUT.lua new file mode 100644 index 00000000..9ec29544 --- /dev/null +++ b/contrib/exportLUT.lua @@ -0,0 +1,204 @@ +--[[ + This file is part of darktable, + copyright (c) 2020 Noah Clarke + + 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 . +]] +--[[ +Add the following line to .config/darktable/luarc to enable this lightable module: + require "contrib/exportLut" + +Given a haldCLUT identity file this script generates haldCLUTS from all the user's +styles and exports them to a location of their choosing. + +Warning: during export if a naming collision occurs the older file is automatically +overwritten silently. +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require("lib/dtutils.file") +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 + +du.check_min_api_version("5.0.0", "exportLUT") + +local eL = {} +eL.module_installed = false +eL.event_registered = false +eL.widgets = {} + +-- Thanks Kevin Ertel for this bit +local os_path_seperator = '/' +if dt.configuration.running_os == 'windows' then os_path_seperator = '\\' end +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"), + value = "", + is_directory = false +} + +local export_chooser_button = dt.new_widget("file_chooser_button"){ + title = _("choose the export location"), + value = "", + is_directory = true +} + +local identity_label = dt.new_widget("label"){ + label = _("choose the identity haldclut file") +} + +local output_label = dt.new_widget("label"){ + label = _("choose the output location") +} + +local warning_label = dt.new_widget("label"){ + label = _("WARNING: files may be silently overwritten") +} + +local function end_job(job) + job.valid = false +end + +local function output_path(style_name, job) + local output_location = export_chooser_button.value .. os_path_seperator .. style_name .. ".png" + output_location = string.gsub(output_location, "|", os_path_seperator) + local output_dir = string.reverse(output_location) + output_dir = string.gsub(output_dir, ".-" .. os_path_seperator, os_path_seperator, 1) + output_dir = string.reverse(output_dir) + if(output_dir ~= "") then + df.mkdir(df.sanitize_filename(output_dir)) + end + return output_location +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")) + else + local job = dt.gui.create_job(_('exporting styles as haldCLUTs'), true, end_job) + + local size = 1 + + for style_num, style in ipairs(dt.styles) do + size = size + 1 + end + + local count = 0 + for style_num, style in ipairs(dt.styles) do + + identity:reset() + dt.styles.apply(style, identity) + local io_lut = dt.new_format("png") + io_lut.bpp = 8 + + 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))) + end + dt.print(_("done exporting haldCLUTs")) + job.valid = false + identity:reset() + end +end + +local function install_module() + if not eL.module_installed then + dt.register_lib( + "export haldclut", + _("export haldclut"), + true, + false, + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, + dt.new_widget("box") + { + orientation = "vertical", + table.unpack(eL.widgets), + }, + nil, + nil + ) + eL.module_installed = true + end +end + +local function destroy() + dt.gui.libs["export haldclut"].visible = false +end + +local function restart() + dt.gui.libs["export haldclut"].visible = true +end + +local export_button = dt.new_widget("button"){ + label = _("export"), + clicked_callback = export_luts +} + +table.insert(eL.widgets, identity_label) +table.insert(eL.widgets, file_chooser_button) +table.insert(eL.widgets, output_label) +table.insert(eL.widgets, export_chooser_button) +table.insert(eL.widgets, warning_label) +table.insert(eL.widgets, export_button) + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not eL.event_registered then + dt.register_event( + "exportLUT", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + eL.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 diff --git a/contrib/ext_editor.lua b/contrib/ext_editor.lua new file mode 100644 index 00000000..be371e92 --- /dev/null +++ b/contrib/ext_editor.lua @@ -0,0 +1,515 @@ +--[[ + ext_editor.lua - edit images with external editors + + 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 . +]] + +--[[ + ext_editor.lua - edit images with external editors + + This script provides helpers to edit image files with programs external to darktable. It adds: + - a new target storage "collection". Image exported will be reimported to collection for further edit with external programs + - a new module "external editors", visible in lightable and darkroom, to select a program from a list + - of up to 9 external editors and run it on a selected image (adjust this limit by changing MAX_EDITORS) + - a set of lua preferences in order to configure name and path of up to 9 external editors + - a set of lua shortcuts in order to quick launch the external editors + + USAGE + * require this script from main lua file + + -- setup -- + * in "preferences/lua options" configure name and path/command of external programs + * note that if a program name is left empty, that and all following entries will be ignored + * in "preferences/shortcuts/lua" configure shortcuts for external programs (optional) + * whenever programs preferences are changed, in external editors GUI, press "update list" + + -- use -- + * in the export dialog choose "collection" and select the format and bit depth for the + exported image + * press "export" + * the exported image will be imported into collection and grouped with the original image + + * in lighttable, select an image for editing with en external program + * (or in darkroom for the image being edited): + * in external editors GUI, select program and press "edit" + * edit the image with the external editor, overwite the file, quit the external program + * the selected image will be updated + or + * in external editors GUI, select program and press "edit a copy" + * edit the image with the external editor, overwite the file, quit the external program + * a copy of the selected image will be created and updated + or + * use the shortcut to edit the current image with the corresponding external editor + * overwite the file, quit the external program + * the image will be updated + + * warning: mouseover on lighttable/filmstrip will prevail on current image + * this is the default DT behavior, not a bug of this script + + CAVEATS + * MAC compatibility not tested + + BUGS, COMMENTS, SUGGESTIONS + * send to Marco Carrarini, marco.carrarini@gmail.com +]] + + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local dtsys = require "lib/dtutils.system" + + +-- module name +local MODULE_NAME = "ext_editor" +du.check_min_api_version("7.0.0", MODULE_NAME) + +-- translation +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("external editors"), + purpose = _("edit images with external editors"), + author = "Marco Carrarini, marco.carrarini@gmail.com", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/ext_editor" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +-- OS compatibility +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +-- namespace +local ee = {} +ee.module_installed = false +ee.event_registered = false +ee.widgets = {} + + +-- maximum number of external programs, can be increased to necessity +local MAX_EDITORS = 9 + +-- number of valid entries in the list of external programs +local n_entries + + +-- allowed file extensions for external editors +local allowed_file_types = {"JPG", "jpg", "JPEG", "jpeg", "TIF", "tif", "TIFF", "tiff", "EXR", "exr", "PNG", "png"} + + +-- last used editor initialization +if not dt.preferences.read(MODULE_NAME, "initialized", "bool") then + dt.preferences.write(MODULE_NAME, "lastchoice", "integer", 0) + dt.preferences.write(MODULE_NAME, "initialized", "bool", true) +end +local lastchoice = 0 + + +-- update lists of program names and paths, as well as combobox --------------- +local function UpdateProgramList(combobox, button_edit, button_edit_copy, update_button_pressed) + + -- initialize lists + program_names = {} + program_paths = {} + + -- build lists from preferences + local name + local last = false + n_entries = 0 + for i = 1, MAX_EDITORS do + name = dt.preferences.read(MODULE_NAME, "program_name_"..i, "string") + if (name == "" or name == nil) then last = true end + if last then + if combobox[n_entries + 1] then combobox[n_entries + 1] = nil end -- remove extra combobox entries + else + combobox[i] = i..": "..name + program_names[i] = name + program_paths[i] = df.sanitize_filename(dt.preferences.read(MODULE_NAME, "program_path_"..i, "string")) + n_entries = i + end + end + + lastchoice = dt.preferences.read(MODULE_NAME, "lastchoice", "integer") + if lastchoice == 0 and n_entries > 0 then lastchoice = 1 end + if lastchoice > n_entries then lastchoice = n_entries end + dt.preferences.write(MODULE_NAME, "lastchoice", "integer", lastchoice) + + -- widgets enabled if there is at least one program configured + combobox.selected = lastchoice + local active = n_entries > 0 + combobox.sensitive = active + button_edit.sensitive = active + button_edit_copy.sensitive = active + + if update_button_pressed then dt.print(string.format(_("%d editors configured"), n_entries)) end +end + + +-- callback for buttons "edit" and "edit a copy" ------------------------------ +local function OpenWith(images, choice, copy) + + -- check choice is valid, return if not + if choice > n_entries then + dt.print(_("not a valid choice")) + return + end + + -- check if one image is selected, return if not + if #images ~= 1 then + dt.print(_("please select one image")) + return + end + + local bin = program_paths[choice] + local friendly_name = program_names[choice] + + if dt.configuration.running_os == "macos" then bin = "open -W -a "..bin end + + -- image to be edited + local image + i, image = next(images) + local name = image.path..PS..image.filename + + -- check if image format is allowed + local file_ext = df.get_filetype(image.filename) + local allowed = false + for i,v in pairs(allowed_file_types) do + if v == file_ext then + allowed = true + break + end + end + if not allowed then + dt.print(_("file type not allowed")) + return + end + + -- save image tags, rating and color + local tags = {} + for i, tag in ipairs(dt.tags.get_tags(image)) do + if not (string.sub(tag.name, 1, 9) == "darktable") then table.insert(tags, tag) end + end + local rating = image.rating + local red = image.red + local blue = image.blue + local green = image.green + local yellow = image.yellow + local purple = image.purple + + -- new image + local new_name = name + local new_image = image + + if copy then + + -- create unique filename + new_name = df.create_unique_filename(new_name) + + -- 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)) + 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)) + local result = dtsys.external_command(run_cmd) + if result ~= 0 then + dt.print(string.format(_("error launching %s"), friendly_name)) + return + end + + if copy then + -- import in database and group + new_image = dt.database.import(new_name) + new_image:group_with(image) + else + -- refresh the image view + -- note that only image:drop_cache() is not enough to refresh view in darkroom mode + -- therefore image must be deleted and reimported to force refresh + + -- find the grouping status + local image_leader = image.group_leader + local group_members = image:get_group_members() + local new_leader + local index = nil + local found = false + + -- membership status, three different cases + if image_leader == image then + if #group_members > 1 then + -- case 1: image is leader in a group with more members + while not found do + index, new_leader = next(group_members, index) + if new_leader ~= image_leader then found = true end + end + new_leader:make_group_leader() + image:delete() + if image.local_copy then image:drop_cache() end -- to fix fail to allocate cache error + new_image = dt.database.import(name) + new_image:group_with(new_leader) + new_image:make_group_leader() + else + -- case 2: image is the only member in group + image:delete() + if image.local_copy then image:drop_cache() end -- to fix fail to allocate cache error + new_image = dt.database.import(name) + new_image:group_with() + end + else + -- case 3: image is in a group but is not leader + image:delete() + if image.local_copy then image:drop_cache() end -- to fix fail to allocate cache error + new_image = dt.database.import(name) + new_image:group_with(image_leader) + end + end + + -- restore image tags, rating and color + for i, tag in ipairs(tags) do dt.tags.attach(tag, new_image) end + new_image.rating = rating + new_image.red = red + new_image.blue = blue + new_image.green = green + new_image.yellow = yellow + new_image.purple = purple + + -- select the new image + local selection = {} + table.insert(selection, new_image) + dt.gui.selection(selection) + + -- refresh darkroom view + if dt.gui.current_view().id == "darkroom" then + dt.gui.views.darkroom.display_image(new_image) + end +end + + +-- callback function for shortcuts -------------------------------------------- +local function program_shortcut(event, shortcut) + OpenWith(dt.gui.action_images, tonumber(string.sub(shortcut, -2)), false) +end + + +-- export images and reimport in collection ----------------------------------- +local function export2collection(storage, image_table, extra_data) + + local temp_name, new_name, new_image, move_success + + for image, temp_name in pairs(image_table) do + + -- images are first exported in temp folder then moved to collection folder + + -- create unique filename + new_name = image.path..PS..df.get_filename(temp_name) + new_name = df.create_unique_filename(new_name) + + -- 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)) + return + end + + -- import in database and group + new_image = dt.database.import(new_name) + new_image:group_with(image.group_leader) + end + + dt.print(_("finished exporting")) +end + + +-- install the module in the UI ----------------------------------------------- +local function install_module(dr) + + local views = {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 90}} + if dr then + views = {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 90}, + [dt.gui.views.darkroom] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 100}} + end + + if not ee.module_installed then + -- register new module "external editors" in lighttable and darkroom ---- + dt.register_lib( + MODULE_NAME, + _("external editors"), + true, -- expandable + false, -- resetable + views, + dt.new_widget("box") { + orientation = "vertical", + table.unpack(ee.widgets), + }, + nil, -- view_enter + nil -- view_leave + ) + ee.module_installed = true + end +end + +local function destroy() + for i = 1, MAX_EDITORS do + dt.destroy_event(MODULE_NAME .. i, "shortcut") + end + dt.destroy_storage("exp2coll") + dt.gui.libs[MODULE_NAME].visible = false +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)) + end + dt.register_storage("exp2coll", _("collection"), nil, export2collection) + dt.gui.libs[MODULE_NAME].visible = true +end + +local function show() + dt.gui.libs[MODULE_NAME].visible = true +end + + +-- combobox, with variable number of entries ---------------------------------- +local combobox = dt.new_widget("combobox") { + label = _("choose program"), + tooltip = _("select the external editor from the list"), + changed_callback = function(self) + dt.preferences.write(MODULE_NAME, "lastchoice", "integer", self.selected) + end, + "" +} + + +-- button edit ---------------------------------------------------------------- +local button_edit = dt.new_widget("button") { + label = _("edit"), + tooltip = _("open the selected image in external editor"), + --sensitive = false, + clicked_callback = function() + OpenWith(dt.gui.action_images, combobox.selected, false) + end +} + + +-- button edit a copy --------------------------------------------------------- +local button_edit_copy = dt.new_widget("button") { + label = _("edit a copy"), + tooltip = _("create a copy of the selected image and open it in external editor"), + clicked_callback = function() + OpenWith(dt.gui.action_images, combobox.selected, true) + end +} + + +-- button update list --------------------------------------------------------- +local button_update_list = dt.new_widget("button") { + label = _("update list"), + tooltip = _("update list of programs if lua preferences are changed"), + clicked_callback = function() + UpdateProgramList(combobox, button_edit, button_edit_copy, true) + end +} + + +-- box for the buttons -------------------------------------------------------- +-- it doesn't seem there is a way to make the buttons equal in size +local box1 = dt.new_widget("box") { + orientation = "horizontal", + button_edit, + button_edit_copy, + button_update_list +} + + +-- table with all the widgets -------------------------------------------------- +table.insert(ee.widgets, combobox) +table.insert(ee.widgets, box1) + + +-- register new module, but only when in lighttable ---------------------------- +local show_dr = dt.preferences.read(MODULE_NAME, "show_in_darkrooom", "bool") +if dt.gui.current_view().id == "lighttable" then + install_module(show_dr) +else + if not ee.event_registered then + dt.register_event( + MODULE_NAME, "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module(show_dr) + end + end + ) + ee.event_registered = true + end +end + + +-- initialize list of programs and widgets ------------------------------------ +UpdateProgramList(combobox, button_edit, button_edit_copy, false) + + +-- register new storage ------------------------------------------------------- +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)")) + + dt.preferences.register(MODULE_NAME, "program_name_"..i, "string", + string.format(_("name of external editor %d"), i), + _("friendly name of external editor"), "") +end +dt.preferences.register(MODULE_NAME, "show_in_darkrooom", "bool", + _("show external editors in darkroom"), + _("check to show external editors module also in darkroom (requires restart)"), false) + + +-- 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)) +end + + +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = show + +return script_data + +-- end of script -------------------------------------------------------------- + +-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua +-- kate: hl Lua; diff --git a/contrib/face_recognition.lua b/contrib/face_recognition.lua index 5f885914..0c60a5c1 100644 --- a/contrib/face_recognition.lua +++ b/contrib/face_recognition.lua @@ -41,52 +41,141 @@ This plugin will add a new storage option and calls face_recognition after expor ]] local dt = require "darktable" +local du = require "lib/dtutils" local df = require "lib/dtutils.file" -local gettext = dt.gettext +local dtsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext --- works with darktable API version from 2.0.0 to 5.0.0 -dt.configuration.check_version(...,{2,0,0},{3,0,0},{4,0,0},{5,0,0}) +-- constants --- 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 MODULE = "face_recognition" +local PS = dt.configuration.running_os == "windows" and '\\' or '/' +local OUTPUT = dt.configuration.tmp_dir .. PS .. "facerecognition.txt" + +du.check_min_api_version("7.0.0", MODULE) local function _(msgid) - return gettext.dgettext("face_recognition", msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("face recognition"), + purpose = _("use facial recognition to tag images"), + author = "Sebastian Witt", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/face_recognition" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +-- namespace + +local fc = {} +fc.module_installed = false +fc.event_registered = false + +local function build_image_table(images) + local image_table = {} + local file_extension = "" + local tmp_dir = dt.configuration.tmp_dir .. PS + local ff = fc.export_format.value + local cnt = 0 + + -- check for plugin-data and direct_edit and build image table accordingly + + if string.match(ff, "JPEG") then + file_extension = ".jpg" + elseif string.match(ff, "PNG") then + file_extension = ".png" + elseif string.match(ff, "TIFF") then + file_extension = ".tif" + end + + for _,img in ipairs(images) do + if img ~= nil then + image_table[tmp_dir .. df.get_basename(img.filename) .. file_extension] = img + cnt = cnt + 1 + end + end + + return image_table, cnt +end + +local function stop_job(job) + job.valid = false +end + +local function do_export(img_tbl, images) + local exporter = nil + local upsize = false + local ff = fc.export_format.value + local height = dt.preferences.read(MODULE, "max_height", "integer") + local width = dt.preferences.read(MODULE, "max_width", "integer") + + -- get the export format parameters + if string.match(ff, "JPEG") then + exporter = dt.new_format("jpeg") + exporter.quality = 80 + elseif string.match(ff, "PNG") then + exporter = dt.new_format("png") + exporter.bpp = 8 + elseif string.match(ff, "TIFF") then + exporter = dt.new_format("tiff") + exporter.bpp = 8 + end + exporter.max_height = height + exporter.max_width = width + + -- export the images + local job = dt.gui.create_job(_("export images"), true, stop_job) + local exp_cnt = 0 + local percent_step = 1.0 / 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)) + exporter:write_image(img, export, upsize) + job.percent = job.percent + percent_step + end + job.valid = false + + -- return success, or not + return true end --- Preference: Tag for unknown_person -dt.preferences.register("FaceRecognition", - "unknownTag", - "string", -- type - _("Face recognition: Unknown tag"), -- label - _("Tag for faces that are not recognized"), -- tooltip - "unknown_person") --- Preference: Images with this substring in tags are ignored -dt.preferences.register("FaceRecognition", - "ignoreTags", - "string", -- type - _("Face recognition: Ignore tag"), -- label - _("Images with this substring in tags are ignored, separate multiple strings with ,"), -- tooltip - "") --- Preference: Number of CPU cores to use -dt.preferences.register("FaceRecognition", - "nrCores", - "integer", -- type - _("Face recognition: Nr of CPU cores"), -- label - _("Number of CPU cores to use, 0 for all"), -- tooltip - 0, -- default - 0, -- min - 64) -- max --- Preference: Known faces path -dt.preferences.register("FaceRecognition", - "knownImagePath", - "directory", -- type - _("Face recognition: Known images"), -- label - _("Path to images with known faces, files named after tag to apply"), -- tooltip - "~/.config/darktable/face_recognition") -- default -- default - -local function show_status (storage, image, format, filename, number, total, high_quality, extra_data) - dt.print("Export to Face recognition "..tostring(number).."/"..tostring(total)) +local function save_preferences() + dt.preferences.write(MODULE, "unknown_tag", "string", fc.unknown_tag.text) + dt.preferences.write(MODULE, "no_persons_found_tag", "string", fc.no_persons_found_tag.text) + dt.preferences.write(MODULE, "ignore_tags", "string", fc.ignore_tags.text) + dt.preferences.write(MODULE, "category_tags", "string", fc.category_tags.text) + dt.preferences.write(MODULE, "known_image_path", "directory", fc.known_image_path.value) + local val = fc.tolerance.value + val = string.gsub(tostring(val), ",", ".") + dt.preferences.write(MODULE, "tolerance", "float", tonumber(val)) + dt.preferences.write(MODULE, "num_cores", "integer", fc.num_cores.value) + dt.preferences.write(MODULE, "export_format", "integer", fc.export_format.selected) + dt.preferences.write(MODULE, "max_width", "integer", tonumber(fc.width.text)) + dt.preferences.write(MODULE, "max_height", "integer", tonumber(fc.height.text)) +end + +local function reset_preferences() + fc.unknown_tag.text = "unknown_person" + fc.no_persons_found_tag.text = "no_persons_found" + fc.ignore_tags.text = "" + fc.category_tags.text = "" + fc.known_image_path.value = dt.configuration.config_dir .. "/face_recognition" + fc.tolerance.value = 0.6 + fc.num_cores.value = -1 + fc.export_format.selected = 1 + fc.width.text = 1000 + fc.height.text = 1000 + save_preferences() end -- Check if image has ignored tag attached @@ -100,116 +189,352 @@ local function ignoreByTag (image, ignoreTags) if string.find (t.name, it, 1, true) then -- The image has ignored tag attached ignoreImage = true - dt.print_error ("Face recognition: Ignored tag: " .. it .. " found in " .. image.id .. ":" .. t.name) + dt.print_log ("Face recognition: Ignored tag: " .. it .. " found in " .. image.id .. ":" .. t.name) end end end - + return ignoreImage end -local function face_recognition (storage, image_table, extra_data) --finalize - if not df.check_if_bin_exists("face_recognition") then - dt.print(_("Face recognition not found")) +local function cleanup(img_list) + for _, img in ipairs(img_list) do + os.remove(img) + end + os.remove(OUTPUT) +end + +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")) return end + save_preferences() + -- Get preferences - local knownPath = dt.preferences.read("FaceRecognition", "knownImagePath", "directory") - local nrCores = dt.preferences.read("FaceRecognition", "nrCores", "integer") - local ignoreTagString = dt.preferences.read("FaceRecognition", "ignoreTags", "string") - local unknownTag = dt.preferences.read("FaceRecognition", "unknownTag", "string") + local knownPath = dt.preferences.read(MODULE, "known_image_path", "directory") + local nrCores = dt.preferences.read(MODULE, "num_cores", "integer") + local ignoreTagString = dt.preferences.read(MODULE, "ignore_tags", "string") + local categoryTagString = dt.preferences.read(MODULE, "category_tags", "string") + local unknownTag = dt.preferences.read(MODULE, "unknown_tag", "string") + local nonpersonsfoundTag = dt.preferences.read(MODULE, "no_persons_found_tag", "string") -- face_recognition uses -1 for all cores, we use 0 in preferences if nrCores < 1 then nrCores = -1 end - + -- Split ignore tags (if any) ignoreTags = {} for tag in string.gmatch(ignoreTagString, '([^,]+)') do table.insert (ignoreTags, tag) - dt.print_error ("Face recognition: Ignore tag: " .. tag) + dt.print_log ("Face recognition: Ignore tag: " .. tag) end - + -- list of exported images - local img_list = {} - for img,v in pairs(image_table) do - table.insert (img_list, v) - end + local image_table, cnt = build_image_table(dt.gui.action_images) - -- Get path of exported images - local path = df.get_path (img_list[1]) - dt.print_error ("Face recognition: Path to unknown images: " .. path) + if cnt > 0 then + local success = do_export(image_table, cnt) + if success then + -- do the face recognition + local img_list = {} - -- Output file - local output = path .. "facerecognition.txt" - - local command = "face_recognition --cpus " .. nrCores .. " " .. knownPath .. " " .. path .. " > " .. output - dt.print_error("Face recognition: Running command: " .. command) - dt.print(_("Starting face recognition...")) + for v,_ in pairs(image_table) do + table.insert (img_list, v) + end - dt.control.execute(command) + -- Get path of exported images + local path = df.get_path (img_list[1]) + dt.print_log ("Face recognition: Path to known faces: " .. knownPath) + dt.print_log ("Face recognition: Path to unknown images: " .. path) + dt.print_log ("Face recognition: Tag used for unknown faces: " .. unknownTag) + dt.print_log ("Face recognition: Tag used if non person is found: " .. nonpersonsfoundTag) + os.setlocale("C") + local tolerance = dt.preferences.read(MODULE, "tolerance", "float") - -- Remove exported images - for _,v in ipairs(img_list) do - os.remove (v) - end - - -- Open output file - local f = io.open(output, "rb") - - if not f then - dt.print(_("Face recognition failed")) - else - dt.print(_("Face recognition finished")) - f:close () - end - - -- Read output - local result = {} - for line in io.lines(output) do - local file, tag = string.match (line, "(.*),(.*)$") - tag = string.gsub (tag, "%d*$", "") - dt.print_error ("File:"..file .." Tag:".. tag) - if result[file] ~= nil then - table.insert (result[file], tag) - else - result[file] = {tag} - end - end - - -- Attach tags - for file,tags in pairs(result) do - -- Find image in table - for img,file2 in pairs(image_table) do - if file == file2 then - for _,t in ipairs (tags) do - -- Check if image is ignored - if ignoreByTag (img, ignoreTags) then - dt.print_error("Face recognition: Ignoring image with ID " .. img.id) + 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...")) + + dtsys.external_command(command) + + -- Open output file + local f = io.open(OUTPUT, "rb") + + if not f then + dt.print(_("face recognition failed")) + else + dt.print(_("face recognition finished")) + f:close () + end + + -- Read output + dt.print(_("processing results...")) + local result = {} + local tags_list = {} + local tag_object = {} + for line in io.lines(OUTPUT) do + if not string.match(line, "^WARNING:") and line ~= "" and line ~= nil then + local file, tag = string.match (line, "(.*),(.*)$") + tag = string.gsub (tag, "%d*$", "") + dt.print_log ("File:"..file .." Tag:".. tag) + tag_object = {} + if result[file] == nil then + tag_object[tag] = true + result[file] = tag_object else - -- Check of unrecognized unknown_person - if t == "unknown_person" then - t = unknownTag + tag_object = result[file] + tag_object[tag] = true + result[file] = tag_object + end + end + end + + -- Attach tags + local result_index = 0 + for file,tags in pairs(result) do + result_index = result_index +1 + -- Find image in table + img = image_table[file] + if img == nil then + dt.print_log("Face recognition: Ignoring face recognition entry: " .. file) + else + for t,_ in pairs (tags) do + -- Check if image is ignored + if ignoreByTag (img, ignoreTags) then + dt.print_log("Face recognition: Ignoring image with ID " .. img.id) + else + -- Check of unrecognized unknown_person + if t == "unknown_person" then + t = unknownTag + end + -- Check of unrecognized no_persons_found + if t == "no_persons_found" then + t = nonpersonsfoundTag + end + if t ~= "" and t ~= nil then + if categoryTagString ~= "" and t ~= nonpersonsfoundTag then + t = categoryTagString .. "|" .. t + end + dt.print_log ("ImgId:" .. img.id .. " Tag:".. t) + -- Create tag if it does not exist + if tags_list[t] == nil then + tag = dt.tags.create (t) + tags_list[t] = tag + else + tag = tags_list[t] + end + img:attach_tag (tag) + end end - dt.print_error ("ImgId:" .. img.id .. " Tag:".. t) - -- Create tag if it does not exists - local tag = dt.tags.create (t) - img:attach_tag (tag) end end end + cleanup(img_list) + dt.print_log("img_list cleaned-up") + dt.print_log("face recognition complete") + dt.print(_("face recognition complete")) + else + dt.print(_("image export failed")) + return end + else + dt.print(_("no images selected")) + return + end +end + +local function install_module() + if not fc.module_installed then + dt.register_lib( + MODULE, -- Module name + _("face recognition"), -- Visible name + true, -- expandable + true, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 300}}, -- containers + fc.widget, + nil,-- view_enter + nil -- view_leave + ) + fc.module_installed = true + end +end + +local function destroy() + dt.gui.libs[MODULE].visible = false +end + +local function restart() + dt.gui.libs[MODULE].visible = true +end + +-- build the interface + +fc.unknown_tag = dt.new_widget("entry"){ + text = dt.preferences.read(MODULE, "unknown_tag", "string"), + tooltip = _("tag to be used for unknown person"), + editable = true, +} + +fc.no_persons_found_tag = dt.new_widget("entry"){ + text = dt.preferences.read(MODULE, "no_persons_found_tag", "string"), + tooltip = _("tag to be used when no persons are found"), + editable = true, +} + +fc.ignore_tags = dt.new_widget("entry"){ + text = dt.preferences.read(MODULE, "ignore_tags", "string"), + tooltip = _("tags of images to ignore"), + editable = true, +} + +fc.category_tags = dt.new_widget("entry"){ + text = dt.preferences.read(MODULE, "category_tags", "string"), + tooltip = _("tag category"), + editable = true, +} + +fc.tolerance = dt.new_widget("slider"){ + label = _("tolerance"), + tooltip = _("detection tolerance - 0.6 default - lower if too many faces detected"), + soft_min = 0.0, + hard_min = 0.0, + soft_max = 1.0, + soft_min = 1.0, + step = 0.1, + digits = 1, + value = 0.0, +} + +fc.num_cores = dt.new_widget("slider"){ + label = _("processor cores"), + tooltip = _("number of processor cores to use, 0 for all"), + soft_min = 0, + soft_max = 16, + hard_min = 0, + hard_max = 64, + step = 1, + digits = 0, + value = dt.preferences.read(MODULE, "num_cores", "integer"), +} + +fc.known_image_path = dt.new_widget("file_chooser_button"){ + title = _("known image directory"), + tooltip = _("face data directory"), + value = dt.preferences.read(MODULE, "known_image_path", "directory"), + is_directory = true, + changed_callback = function(this) + dt.preferences.write(MODULE, "known_image_path", "directory", this.value) + end +} + +fc.export_format = dt.new_widget("combobox"){ + label = _("export image format"), + tooltip = _("format for exported images"), + selected = dt.preferences.read(MODULE, "export_format", "integer"), + changed_callback = function(this) + dt.preferences.write(MODULE, "export_format", "integer", this.selected) + end, + "JPEG", "PNG", "TIFF", +} + +fc.width = dt.new_widget("entry"){ + text = tostring(dt.preferences.read(MODULE, "max_width", "integer")), + tooltip = _("maximum exported image width"), + editable = true, +} + +fc.height = dt.new_widget("entry"){ + text = tostring(dt.preferences.read(MODULE, "max_height", "integer")), + tooltip = _("maximum exported image height"), + editable = true, +} + +fc.execute = dt.new_widget("button"){ + label = "detect faces", + clicked_callback = function(this) + face_recognition() end - - --os.remove (output) +} + +local widgets = { + dt.new_widget("label"){ label = _("unknown person tag")}, + fc.unknown_tag, + dt.new_widget("label"){ label = _("no persons found tag")}, + fc.no_persons_found_tag, + dt.new_widget("label"){ label = _("tags of images to ignore")}, + fc.ignore_tags, + dt.new_widget("label"){ label = _("tag category")}, + fc.category_tags, + dt.new_widget("label"){ label = _("face data directory")}, + fc.known_image_path, +} + +if dt.configuration.running_os == "windows" or dt.configuration.running_os == "macos" then + table.insert(widgets, df.executable_path_widget({"face_recognition"})) +end +table.insert(widgets, dt.new_widget("section_label"){ label = _("processing options")}) +table.insert(widgets, fc.tolerance) +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")}, + fc.width, +}) +table.insert(widgets, dt.new_widget("box"){ + orientation = "horizontal", + dt.new_widget("label"){ label = _("height")}, + fc.height, +}) +table.insert(widgets, fc.execute) + +fc.widget = dt.new_widget("box"){ + orientation = vertical, + reset_callback = function(this) + reset_preferences() + end, + table.unpack(widgets), +} + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not fc.event_registered then + dt.register_event( + MODULE, "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + fc.event_registered = true + end +end + +fc.tolerance.value = dt.preferences.read(MODULE, "tolerance", "float") + +-- preferences +if not dt.preferences.read(MODULE, "initialized", "bool") then + reset_preferences() + save_preferences() + dt.preferences.write(MODULE, "initialized", "bool", true) end --- Register -dt.register_storage("module_face_recognition", _("Face recognition"), show_status, face_recognition) +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = restart +return script_data -- -- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua diff --git a/contrib/fujifilm_dynamic_range.lua b/contrib/fujifilm_dynamic_range.lua new file mode 100644 index 00000000..37f79511 --- /dev/null +++ b/contrib/fujifilm_dynamic_range.lua @@ -0,0 +1,156 @@ +--[[ fujifilm_dynamic_range-0.1 + +Compensate for Fujifilm raw files made using "dynamic range". + +Copyright (C) 2021, 2022 Dan Torop + +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 2 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, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +]] + +--[[About this Plugin +Support for adjusting darktable exposure by Fujifilm raw exposure +bias. This corrects for a DR100/DR200/DR400 "dynamic range" setting. + +Dependencies: +- exiftool (https://www.sno.phy.queensu.ca/~phil/exiftool/) + +Based upon fujifilm_ratings by Ben Mendis + +The relevant tag is RawExposureBias (0x9650). This appears to +represent the shift in EV for the chosen DR setting (whether manual or +automatic). Note that even at 100DR ("standard") there is an EV shift: + +100 DR -> -0.72 EV +200 DR -> -1.72 EV +400 DR -> -2.72 EV + +The ideal would be to use exiv2 to read this tag, as this is the same +code which darktable import uses. Unfortunately, exiv2 as of v0.27.3 +can't read this tag. As it is encoded as a 4-byte ratio of two signed +shorts -- a novel data type -- it will require some attention to fix +this. + +There is an exiv2-readable DevelopmentDynamicRange tag which maps to +RawExposureBias as above. DevelopmentDynamicRange is only present +when tag DynamicRangeSetting (0x1402) is Manual/Raw (0x0001). When it +is Auto (0x0000), the equivalent data is tag AutoDynamicRange +(0x140b). But exiv2 currently can't read that tag either. + +Hence for now this code uses exiftool to read RawExposureBias, as a +more general solution. As exiftool is approx. 10x slower than exiv2 +(Perl vs. C++), this may slow large imports. + +These tags have been checked on a Fujifilm X100S and X100V. Other +cameras may behave in other ways. + +--]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local dtsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext + +du.check_min_api_version("7.0.0", "fujifilm_dynamic_range") + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("fujifilm dynamic range"), + purpose = _("compensate for Fujifilm raw files made using \"dynamic range\""), + author = "Dan Torop ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/fujifilm_dynamic_range" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +local function detect_dynamic_range(event, image) + if image.exif_maker ~= "FUJIFILM" then + dt.print_log("[fujifilm_dynamic_range] ignoring non-Fujifilm image") + return + end + -- it would be nice to check image.is_raw but this appears to not yet be set + if not string.match(image.filename, "%.RAF$") then + dt.print_log("[fujifilm_dynamic_range] ignoring non-raw image") + return + end + local command = df.check_if_bin_exists("exiftool") + if not command then + dt.print_error("[fujifilm_dynamic_range] exiftool not found") + return + end + local RAF_filename = df.sanitize_filename(tostring(image)) + -- without -n flag, exiftool will round to the nearest tenth + command = command .. " -RawExposureBias -n -t " .. RAF_filename + dt.print_log(command) + output = io.popen(command) + local raf_result = output:read("*all") + output:close() + if #raf_result == 0 then + dt.print_error("[fujifilm_dynamic_range] no output returned by exiftool") + return + end + raf_result = string.match(raf_result, "\t(.*)") + if not raf_result then + dt.print_error("[fujifilm_dynamic_range] could not parse exiftool output") + return + end + if image.exif_exposure_bias ~= image.exif_exposure_bias then + -- is NAN (this is unlikely as RAFs should have ExposureBiasValue set) + image.exif_exposure_bias = 0 + end + -- this should be auto-applied if plugins/darkroom/workflow is scene-referred + -- 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() + dt.destroy_event("fujifilm_dr", "post-import-image") +end + +if not df.check_if_bin_exists("exiftool") then + dt.print_log("Please install exiftool to use fujifilm_dynamic_range") + error "[fujifilm_dynamic_range] exiftool not found" +end + +dt.register_event("fujifilm_dr", "post-import-image", + detect_dynamic_range) + +dt.print_log("[fujifilm_dynamic_range] loaded") + +script_data.destroy = destroy + +return script_data diff --git a/contrib/fujifilm_ratings.lua b/contrib/fujifilm_ratings.lua index e19ef79b..b29996d9 100644 --- a/contrib/fujifilm_ratings.lua +++ b/contrib/fujifilm_ratings.lua @@ -24,47 +24,72 @@ Dependencies: --]] local dt = require "darktable" +local du = require "lib/dtutils" local df = require "lib/dtutils.file" -local gettext = dt.gettext +local dtsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext -dt.configuration.check_version(..., {4,0,0}) - -gettext.bindtextdomain("fujifilm_ratings", dt.configuration.config_dir.."/lua/locale/") +du.check_min_api_version("7.0.0", "fujifilm_ratings") local function _(msgid) - return gettext.dgettext("fujifilm_ratings", msgid) + return gettext(msgid) end +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("fujifilm ratings"), + purpose = _("import Fujifilm in-camera ratings"), + author = "Ben Mendis ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/fujifilm_ratings" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + local function detect_rating(event, image) + if not string.match(image.filename, "%.RAF$") and not string.match(image.filename, "%.raf$") then + return + end if not df.check_if_bin_exists("exiftool") then - dt.print_error(_("exiftool not found")) + dt.print_error("exiftool not found") return end - local RAF_filename = tostring(image) + local RAF_filename = df.sanitize_filename(tostring(image)) local JPEG_filename = string.gsub(RAF_filename, "%.RAF$", ".JPG") local command = "exiftool -Rating " .. JPEG_filename - dt.print_error(command) + dt.print_log(command) local output = io.popen(command) local jpeg_result = output:read("*all") output:close() if string.len(jpeg_result) > 0 then jpeg_result = string.gsub(jpeg_result, "^Rating.*(%d)", "%1") image.rating = tonumber(jpeg_result) - dt.print_error(_("Using JPEG Rating: ") .. tostring(jpeg_result)) + dt.print_log("using JPEG rating: " .. jpeg_result) return end command = "exiftool -Rating " .. RAF_filename - dt.print_error(command) + dt.print_log(command) output = io.popen(command) local raf_result = output:read("*all") output:close() if string.len(raf_result) > 0 then raf_result = string.gsub(raf_result, "^Rating.*(%d)", "%1") image.rating = tonumber(raf_result) - dt.print_error(_("Using RAF Rating: ") .. tostring(raf_result)) + dt.print_log("using RAF rating: " .. raf_result) end end -dt.register_event("post-import-image", detect_rating) +local function destroy() + dt.destroy_event("fujifilm_rat", "post-import-image") +end + +dt.register_event("fujifilm_rat", "post-import-image", + detect_rating) -print(_("fujifilm_ratings loaded.")) +script_data.destroy = destroy +return script_data diff --git a/contrib/geoJSON_export.lua b/contrib/geoJSON_export.lua index 6fc76fa6..93e77289 100644 --- a/contrib/geoJSON_export.lua +++ b/contrib/geoJSON_export.lua @@ -33,19 +33,33 @@ USAGE ]] local dt = require "darktable" +local du = require "lib/dtutils" local df = require "lib/dtutils.file" -require "official/yield" -local gettext = dt.gettext +local dtsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext -dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0}) - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("geoJSON_export",dt.configuration.config_dir.."/lua/locale/") +du.check_min_api_version("7.0.0", "geoJSON_export") local function _(msgid) - return gettext.dgettext("geoJSON_export", 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 + -- Sort a table local function spairs(_table, order) -- Code copied from http://stackoverflow.com/questions/15706270/sort-a-table-in-lua -- collect the keys @@ -71,24 +85,24 @@ local function spairs(_table, order) -- Code copied from http://stackoverflow.co end local function show_status(storage, image, format, filename, number, total, high_quality, extra_data) - dt.print(string.format(_("Export Image %i/%i"), number, total)) + dt.print(string.format(_("export image %i/%i"), number, total)) end local function create_geoJSON_file(storage, image_table, extra_data) if not df.check_if_bin_exists("mkdir") then - dt.print_error(_("mkdir not found")) + dt.print_error("mkdir not found") return end if not df.check_if_bin_exists("convert") then - dt.print_error(_("convert not found")) + dt.print_error("convert not found") return end if not df.check_if_bin_exists("xdg-open") then - dt.print_error(_("xdg-open not found")) + dt.print_error("xdg-open not found") return end if not df.check_if_bin_exists("xdg-user-dir") then - dt.print_error(_("xdg-user-dir not found")) + dt.print_error("xdg-user-dir not found") return end @@ -279,7 +293,7 @@ local function create_geoJSON_file(storage, image_table, extra_data) file:write(geoJSON_file) file:close() - dt.print("geoJSON file created in "..exportDirectory) + dt.print(string.format(_("%s file created in %s"), "geoJSON", exportDirectory)) -- Open the file with the standard programm if ( dt.preferences.read("geoJSON_export","OpengeoJSONFile","bool") == true ) then @@ -291,24 +305,28 @@ local function create_geoJSON_file(storage, image_table, extra_data) end +local function destroy() + dt.destroy_storage("geoJSON_export") +end + -- Preferences dt.preferences.register("geoJSON_export", "CreateMapBoxHTMLFile", "bool", - _("geoJSON export: Create an additional HTML file"), - _("Creates a HTML file, that loads the geoJASON file. (Needs a MapBox key"), + "geoJSON export: ".._("Create an additional HTML file"), + _("creates an HTML file that loads the geoJSON file. (needs a MapBox key"), false ) dt.preferences.register("geoJSON_export", "mapBoxKey", "string", - _("geoJSON export: MapBox Key"), - _("https://www.mapbox.com/studio/account/tokens"), + "geoJSON export: MapBox " .. _("key"), + "https://www.mapbox.com/studio/account/tokens", '' ) dt.preferences.register("geoJSON_export", "OpengeoJSONFile", "bool", - _("geoJSON export: Open geoJSON file after export"), - _("Opens the geoJSON file after the export with the standard program for geoJSON files"), + "geoJSON export: " .. _("open geoJSON file after export"), + _("opens the geoJSON file after the export with the standard program for geoJSON files"), false ) local handle = io.popen("xdg-user-dir DESKTOP") @@ -320,9 +338,13 @@ 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 dt.register_storage("geoJSON_export", "geoJSON Export", nil, create_geoJSON_file) + +script_data.destroy = destroy + +return script_data diff --git a/contrib/geoToolbox.lua b/contrib/geoToolbox.lua index 8a28001c..01a14eea 100644 --- a/contrib/geoToolbox.lua +++ b/contrib/geoToolbox.lua @@ -26,36 +26,56 @@ require "geoToolbox" ]] local dt = require "darktable" +local du = require "lib/dtutils" local df = require "lib/dtutils.file" -require "official/yield" -local gettext = dt.gettext +local dtsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext -dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0}) - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("geoToolbox",dt.configuration.config_dir.."/lua/locale/") +du.check_min_api_version("7.0.0", "geoToolbox") local function _(msgid) - return gettext.dgettext("geoToolbox", 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 + + +local gT = {} +gT.module_installed = false +gT.event_registered = false + + -- local labelDistance = dt.new_widget("label") -labelDistance.label = _("Distance:") +labelDistance.label = _("distance:") local label_copy_gps_lat = dt.new_widget("check_button") { - label = _("latitude:"), + label = _("latitude: "), value = true } local label_copy_gps_lon = dt.new_widget("check_button") { - label = _("longitude:"), + label = _("longitude: "), value = true } local label_copy_gps_ele = dt.new_widget("check_button") { - label = _("elevation:"), + label = _("elevation: "), value = true } -- @@ -147,7 +167,7 @@ local function get_first_coordinate() first_elevation = '' first_image_date = 0 - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if not image then first_have_data = false else @@ -186,7 +206,7 @@ local function get_second_coordinate() second_elevation = '' second_image_date = 0 - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if not image then second_have_data = false else @@ -222,7 +242,7 @@ local calc_in_between_slider = dt.new_widget("slider") --ToDo: this needs more love local function calc_in_between() local sel_images = dt.gui.action_images - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if image then image_date = make_time_stamp(image.exif_datetime_taken) if (first_have_data and second_have_data) then @@ -269,7 +289,7 @@ local function copy_gps() copy_gps_longitude = '' copy_gps_elevation = '' - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if not image then copy_gps_have_data = false else @@ -286,7 +306,7 @@ local function copy_gps() end label_copy_gps_lat.label = _("latitude: ") .. copy_gps_latitude - label_copy_gps_lon.label = _("longitude: ") ..copy_gps_longitude + label_copy_gps_lon.label = _("longitude: ") .. copy_gps_longitude label_copy_gps_ele.label = _("elevation: ") .. copy_gps_elevation return @@ -296,7 +316,7 @@ end local function paste_gps(image) local sel_images = dt.gui.action_images - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if (label_copy_gps_lat.value) then image.latitude = copy_gps_latitude end @@ -323,7 +343,7 @@ local function open_location_in_gnome_maps() local i = 0; -- Use the first image with geo information - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if ((image.longitude and image.latitude) and (image.longitude ~= 0 and image.latitude ~= 90) -- Sometimes the north-pole but most likely just wrong data ) then @@ -350,12 +370,12 @@ end local function reverse_geocode() if not df.check_if_bin_exists("curl") then - dt.print_error(_("curl not found")) + dt.print_error("curl not found") return end if not df.check_if_bin_exists("jq") then - dt.print_error(_("jq not found")) + dt.print_error("jq not found") return end @@ -366,7 +386,7 @@ local function reverse_geocode() local i = 0; -- Use the first image with geo information - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if ((image.longitude and image.latitude) and (image.longitude ~= 0 and image.latitude ~= 90) -- Sometimes the north-pole but most likely just wrong data ) then @@ -416,7 +436,7 @@ local function get_distance(lat1, lon1, ele1, lat2, lon2, ele2) math.cos(math.rad(lat1)) * math.cos(math.rad(lat2)) * math.sin(dLon/2) * math.sin(dLon/2) ; - local angle = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)); + local angle = 2 * math.atan(math.sqrt(a), math.sqrt(1-a)); local distance = earthRadius * angle; -- Distance in km -- Add the elevation to the calculation @@ -441,7 +461,7 @@ local function calc_distance() local sel_images = dt.gui.selection() - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if ((image.longitude and image.latitude) and (image.longitude ~= 0 and image.latitude ~= 90) -- Sometimes the north-pole but most likely just wrong data ) then @@ -473,12 +493,12 @@ local function calc_distance() if (distance < 1) then distance = distance * 1000 - distanceUnit = _("m") + distanceUnit = "m" else - distanceUnit = _("km") + distanceUnit = "km" end - return string.format(_("Distance: %.2f %s"), distance, distanceUnit) + return string.format(_("distance: %.2f %s"), distance, distanceUnit) end local function print_calc_distance() @@ -501,12 +521,12 @@ local altitude_filename = dt.new_widget("entry") text = "altitude.csv", placeholder = "altitude.csv", editable = true, - tooltip = _("Name of the exported file"), + tooltip = _("name of the exported file"), reset_callback = function(self) self.text = "text" end } local function altitude_profile() - dt.print(_("Start export")) + dt.print(_("start export")) local sel_images = dt.gui.action_images local lat1 = 0; @@ -525,7 +545,7 @@ local function altitude_profile() local elevationAdd = 0; local sel_images = dt.gui.action_images - for _,image in ipairs(sel_images) do + for jj,image in ipairs(sel_images) do if ((not isnan(image.longitude) and not isnan(image.latitude) and not isnan(image.elevation) and image.elevation) and (image.longitude ~= 0 and image.latitude ~= 90) -- Sometimes the north-pole but most likely just wrong data ) then @@ -567,10 +587,53 @@ local function altitude_profile() file = io.open(exportDirectory.."/"..exportFilename, "w") file:write(csv_file) file:close() - dt.print(_("File created in ")..exportDirectory) + dt.print(string.format(_("file created in %s"), exportDirectory)) + +end +local function install_module() + if not gT.module_installed then + dt.register_lib( + "geoToolbox", -- Module name + _("geo toolbox"), -- name + true, -- expandable + false, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers + gT.widget, + nil,-- view_enter + nil -- view_leave + ) + gT.module_installed = true + end end +local function destroy() + dt.gui.libs["geoToolbox"].visible = false + dt.destroy_event("geoToolbox_cd", "shortcut") + dt.destroy_event("geoToolbox", "mouse-over-image-changed") + dt.destroy_event("geoToolbox_wg", "shortcut") + dt.destroy_event("geoToolbox_ng", "shortcut") +end + +local function restart() + dt.register_event("geoToolbox_cd", "shortcut", + 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")) + dt.register_event("geoToolbox_ng", "shortcut", + select_without_gps, _("select all images without GPS information")) + + dt.gui.libs["geoToolbox"].visible = true +end + +local function show() + dt.gui.libs["geoToolbox"].visible = true +end + + local separator = dt.new_widget("separator"){} local separator2 = dt.new_widget("separator"){} @@ -578,32 +641,26 @@ local separator3 = dt.new_widget("separator"){} local separator4 = dt.new_widget("separator"){} local separator5 = dt.new_widget("separator"){} -dt.register_lib( - "geoToolbox", -- Module name - "geo toolbox", -- name - true, -- expandable - false, -- resetable - {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers - dt.new_widget("box") +gT.widget = dt.new_widget("box") { orientation = "vertical", dt.new_widget("button") { label = _("select geo images"), - tooltip = _("Select all images with GPS information"), + tooltip = _("select all images with GPS information"), clicked_callback = select_with_gps }, dt.new_widget("button") { label = _("select non-geo images"), - tooltip = _("Select all images without GPS information"), + tooltip = _("select all images without GPS information"), clicked_callback = select_without_gps }, separator,-------------------------------------------------------- dt.new_widget("button") { label = _("copy GPS data"), - tooltip = _("Copy GPS data"), + tooltip = _("copy GPS data"), clicked_callback = copy_gps }, label_copy_gps_lat, @@ -612,7 +669,7 @@ dt.register_lib( dt.new_widget("button") { label = _("paste GPS data"), - tooltip = _("Paste GPS data"), + tooltip = _("paste GPS data"), clicked_callback = paste_gps }, separator2,-------------------------------------------------------- @@ -642,14 +699,14 @@ dt.register_lib( 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,-------------------------------------------------------- @@ -659,29 +716,53 @@ dt.register_lib( 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 - }, - nil,-- view_enter - nil -- view_leave -) + } + + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not gT.event_registered then + dt.register_event( + "geoToolbox", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + gT.event_registered = true + end +end -- Preferences 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("shortcut", print_calc_distance, _("Calculate the distance from latitude and longitude in km")) -dt.register_event("mouse-over-image-changed", toolbox_calc_distance) - -dt.register_event("shortcut", select_with_gps, _("Select all images with GPS information")) -dt.register_event("shortcut", select_without_gps, _("Select all images without GPS information")) - +dt.register_event("geoToolbox_cd", "shortcut", + 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")) +dt.register_event("geoToolbox_ng", "shortcut", + select_without_gps, _("select all images without GPS information")) + +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = show + +return script_data -- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua -- kate: hl Lua; diff --git a/contrib/gimp.lua b/contrib/gimp.lua index f8472c75..92c6d0ce 100644 --- a/contrib/gimp.lua +++ b/contrib/gimp.lua @@ -41,7 +41,9 @@ * require this script from your main lua file * select an image or images for editing with GIMP * in the export dialog select "Edit with GIMP" and select the format and bit depth for the - exported image + exported image. Check the "run_detached" button to run GIMP in detached mode. Images + will not be returned to darktable in this mode, but additional images can be sent to + GIMP without stopping it. * Press "export" * Edit the image with GIMP then save the changes with File->Overwrite.... * Exit GIMP @@ -64,181 +66,75 @@ ]] local dt = require "darktable" +local du = require "lib/dtutils" local df = require "lib/dtutils.file" -require "official/yield" -local gettext = dt.gettext +local dtsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext +local gimp_widget = nil -dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0}) - --- 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 split_filepath(str) - local result = {} - -- Thank you Tobias Jakobs for the awesome regular expression, which I tweaked a little - result["path"], result["filename"], result["basename"], result["filetype"] = string.match(str, "(.-)(([^\\/]-)%.?([^%.\\/]*))$") - return result -end - -local function get_path(str) - local parts = split_filepath(str) - return parts["path"] -end - -local function get_filename(str) - local parts = split_filepath(str) - return parts["filename"] -end - -local function get_basename(str) - local parts = split_filepath(str) - return parts["basename"] -end - -local function get_filetype(str) - local parts = split_filepath(str) - return parts["filetype"] -end +du.check_min_api_version("7.0.0", "gimp") local function _(msgid) - return gettext.dgettext("gimp", msgid) -end - --- Thanks Tobias Jakobs for the idea and the correction -function checkIfFileExists(filepath) - local file = io.open(filepath,"r") - local ret - if file ~= nil then - io.close(file) - dt.print_error("true checkIfFileExists: "..filepath) - ret = true - else - dt.print_error(filepath.." not found") - ret = false - end - return ret + return gettext(msgid) end -local function filename_increment(filepath) +-- return data structure for script_manager - -- break up the filepath into parts - local path = get_path(filepath) - local basename = get_basename(filepath) - local filetype = get_filetype(filepath) +local script_data = {} - -- check to see if we've incremented before - local increment = string.match(basename, "_(%d-)$") +script_data.metadata = { + name = _("edit with GIMP"), + purpose = _("export and edit with GIMP"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/gimp" +} - if increment then - -- we do 2 digit increments so make sure we didn't grab part of the filename - if string.len(increment) > 2 then - -- we got the filename so set the increment to 01 - increment = "01" - else - increment = string.format("%02d", tonumber(increment) + 1) - basename = string.gsub(basename, "_(%d-)$", "") - end - else - increment = "01" - end - local incremented_filepath = path .. basename .. "_" .. increment .. "." .. filetype - - dt.print_error("original file was " .. filepath) - dt.print_error("incremented file is " .. incremented_filepath) +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 - return incremented_filepath -end - -local function groupIfNotMember(img, new_img) +local function group_if_not_member(img, new_img) local image_table = img:get_group_members() local is_member = false for _,image in ipairs(image_table) do - dt.print_error(image.filename .. " is a member") + dt.print_log(image.filename .. " is a member") if image.filename == new_img.filename then is_member = true - dt.print_error("Already in group") + dt.print_log("Already in group") end end if not is_member then - dt.print_error("group leader is "..img.group_leader.filename) + dt.print_log("group leader is "..img.group_leader.filename) new_img:group_with(img.group_leader) - dt.print_error("Added to group") + dt.print_log("Added to group") end end -local function sanitize_filename(filepath) - local path = get_path(filepath) - local basename = get_basename(filepath) - local filetype = get_filetype(filepath) - - local sanitized = string.gsub(basename, " ", "\\ ") - - return path .. sanitized .. "." .. filetype -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 fileCopy(fromFile, toFile) - local result = nil - -- if cp exists, use it - if df.check_if_bin_exists("cp") then - result = os.execute("cp '" .. fromFile .. "' '" .. toFile .. "'") - end - -- if cp was not present, or if cp failed, then a pure lua solution - if not result then - local fileIn, err = io.open(fromFile, 'rb') - if fileIn then - local fileOut, errr = io.open(toFile, 'w') - if fileOut then - local content = fileIn:read(4096) - while content do - fileOut:write(content) - content = fileIn:read(4096) - end - result = true - fileIn:close() - fileOut:close() - else - dt.print_error("fileCopy Error: " .. errr) - end - else - dt.print_error("fileCopy Error: " .. err) - end - end - return result -end +local function gimp_edit(storage, image_table, extra_data) --finalize -local function fileMove(fromFile, toFile) - local success = os.rename(fromFile, toFile) - if not success then - -- an error occurred, so let's try using the operating system function - if df.check_if_bin_exists("mv") then - success = os.execute("mv '" .. fromFile .. "' '" .. toFile .. "'") - end - -- if the mv didn't exist or succeed, then... - if not success then - -- pure lua solution - success = fileCopy(fromFile, toFile) - if success then - os.remove(fromFile) - else - dt.print_error("fileMove Error: Unable to move " .. fromFile .. " to " .. toFile .. ". Leaving " .. fromFile .. " in place.") - dt.print(string.format(_("Unable to move edited file into collection. Leaving it as %s"), fromFile)) - end - end - end - return success -- nil on error, some value if success -end + local run_detached = dt.preferences.read("gimp", "run_detached", "bool") -local function gimp_edit(storage, image_table, extra_data) --finalize - if not df.check_if_bin_exists("gimp") then - dt.print_error(_("GIMP not found")) + local gimp_executable = df.check_if_bin_exists("gimp") + + if not gimp_executable then + dt.print_error("GIMP not found") return end + if dt.configuration.running_os == "macos" then + if run_detached then + gimp_executable = "open -a " .. gimp_executable + else + gimp_executable = "open -W -a " .. gimp_executable + end + end + -- list of exported images local img_list @@ -246,57 +142,84 @@ local function gimp_edit(storage, image_table, extra_data) --finalize img_list = "" for _,exp_img in pairs(image_table) do - exp_img = sanitize_filename(exp_img) + exp_img = df.sanitize_filename(exp_img) img_list = img_list ..exp_img.. " " end - dt.print(_("Launching GIMP...")) + dt.print(_("launching GIMP...")) local gimpStartCommand - gimpStartCommand = "gimp "..img_list + gimpStartCommand = gimp_executable .. " " .. img_list - dt.print_error(gimpStartCommand) + if run_detached then + if dt.configuration.running_os == "windows" then + gimpStartCommand = "start /b \"\" " .. gimpStartCommand + else + gimpStartCommand = gimpStartCommand .. " &" + end + end - dt.control.execute( gimpStartCommand) + dt.print_log(gimpStartCommand) - -- for each of the image, exported image pairs - -- move the exported image into the directory with the original - -- then import the image into the database which will group it with the original - -- and then copy over any tags other than darktable tags + dtsys.external_command(gimpStartCommand) - for image,exported_image in pairs(image_table) do + if not run_detached then - local myimage_name = image.path .. "/" .. get_filename(exported_image) + -- for each of the image, exported image pairs + -- move the exported image into the directory with the original + -- then import the image into the database which will group it with the original + -- and then copy over any tags other than darktable tags - while checkIfFileExists(myimage_name) do - myimage_name = filename_increment(myimage_name) - -- limit to 99 more exports of the original export - if string.match(get_basename(myimage_name), "_(d-)$") == "99" then - break + for image,exported_image in pairs(image_table) do + + local myimage_name = image.path .. "/" .. df.get_filename(exported_image) + + while df.check_if_file_exists(myimage_name) do + myimage_name = df.filename_increment(myimage_name) + -- limit to 99 more exports of the original export + if string.match(df.get_basename(myimage_name), "_(d-)$") == "99" then + break + end end - end - dt.print_error("moving " .. exported_image .. " to " .. myimage_name) - local result = fileMove(exported_image, myimage_name) + dt.print_log("moving " .. exported_image .. " to " .. myimage_name) + local result = df.file_move(exported_image, myimage_name) - if result then - dt.print_error("importing file") - local myimage = dt.database.import(myimage_name) + if result then + dt.print_log("importing file") + local myimage = dt.database.import(myimage_name) - groupIfNotMember(image, myimage) + group_if_not_member(image, myimage) - for _,tag in pairs(dt.tags.get_tags(image)) do - if not (string.sub(tag.name,1,9) == "darktable") then - dt.print_error("attaching tag") - dt.tags.attach(tag,myimage) + for _,tag in pairs(dt.tags.get_tags(image)) do + if not (string.sub(tag.name,1,9) == "darktable") then + dt.print_log("attaching tag") + dt.tags.attach(tag,myimage) + end end end end end +end +local function destroy() + dt.destroy_storage("module_gimp") end -- Register -dt.register_storage("module_gimp", _("Edit with GIMP"), show_status, gimp_edit) + +gimp_widget = dt.new_widget("check_button"){ + label = _("run detached"), + tooltip = _("don't import resulting image back into darktable"), + value = dt.preferences.read("gimp", "run_detached", "bool"), + clicked_callback = function(this) + dt.preferences.write("gimp", "run_detached", "bool", this.value) + end +} + +dt.register_storage("module_gimp", _("edit with GIMP"), show_status, gimp_edit, nil, nil, gimp_widget) -- +script_data.destroy = destroy + +return script_data diff --git a/contrib/gpx_export.lua b/contrib/gpx_export.lua new file mode 100644 index 00000000..b310f3a9 --- /dev/null +++ b/contrib/gpx_export.lua @@ -0,0 +1,213 @@ +--[[ + This file is part of darktable, + copyright (c) 2017 Jannis_V + + 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 . +]] + +--[[ +Simple darktable GPX generator script + +This script generates a GPX track from all images having GPS latitude +and longitude information. +For each source folder, a separate is generated in the gpx file. +]] + +local dt = require "darktable" +local df = require "lib/dtutils.file" +local dl = require "lib/dtutils" +local gettext = dt.gettext.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 + +local gpx = {} +gpx.module_installed = false +gpx.event_registered = false + +local path_entry = dt.new_widget("entry") +{ + text = dt.preferences.read("gpx_exporter", "gpxExportPath", "string"), + editable=true, + reset_callback = function(self) + self.text = "~/darktable.gpx" + dt.preferences.write("gpx_exporter", "gpxExportPath", "string", self.text) + end, + tooltip = _("gpx file path"), +} + +local function stop_job(job) + job.valid = false +end + +local function create_gpx_file() + dt.preferences.write("gpx_exporter", "gpxExportPath", "string", path_entry.text) + + path = path_entry.text:gsub("^~", os.getenv("HOME")) -- Expand ~ to home + path = path:gsub("//", "/") + + dt.print(_("exporting gpx file...")) + + job = dt.gui.create_job(_("gpx export"), true, stop_job) + + local sel_images = dt.gui.action_images + local segments = {} + for key,image in dl.spairs(sel_images, function(t, a, b) return t[b].path > t[a].path end) do + + print(image.path) + + if (job.valid) then + job.percent = (key - 1) / #sel_images + + if ((image.longitude and image.latitude) and + (image.longitude ~= 0 and image.latitude ~= 90) -- Just in case + ) then + if (segments[image.path] == nil) then + segments[image.path] = {} + end + + if (image.exif_datetime_taken == "") then + dt.print(image.path.."/"..image.filename.._(" does not have date information and won't be processed")) + print(image.path.."/"..image.filename.._(" does not have date information and won't be processed")) -- Also print to terminal + else + segments[image.path][image.filename] = image + end + end + else + break + end + end + + local gpx_file = "\n" + gpx_file = gpx_file.."\n" + + for key, folder in dl.spairs(segments) do + gpx_file = gpx_file.."\t\n" + gpx_file = gpx_file.."\t\t"..key.."\n"; + gpx_file = gpx_file.."\t\t\n"; + for key2, image in dl.spairs(folder, function(t, a, b) return t[b].exif_datetime_taken > t[a].exif_datetime_taken end) do + date_format = "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)" + my_year, my_month, my_day, my_hour, my_min, my_sec = image.exif_datetime_taken:match(date_format) + + local my_timestamp = os.time({year=my_year, month=my_month, day=my_day, hour=my_hour, min=my_min, sec=my_sec}) + + gpx_file = gpx_file.."\t\t\t\n" + gpx_file = gpx_file.."\t\t\t\t\n" + gpx_file = gpx_file.."\t\t\t\t"..image.path.."/"..image.filename.."\n" + gpx_file = gpx_file.."\t\t\t\n" + end + gpx_file = gpx_file.."\t\t\n"; + gpx_file = gpx_file.."\t\n"; + end + + job.valid = false + + gpx_file = gpx_file.."\n"; + + local file = io.open(path, "w") + if (file == nil) then + dt.print(string.format(_("invalid path: %s"), path)) + else + file:write(gpx_file) + file:close() + dt.print(string.format(_("gpx file created: "), path)) + end +end + +local function install_module() + if not gpx.module_installed then + dt.register_lib( + "gpx_exporter", + _("gpx export"), + true, -- expandable + true, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers + gpx.widget, + nil,-- view_enter + nil -- view_leave + ) + gpx.module_installed = true + end +end + +local function destroy() + dt.gui.libs["gpx_exporter"].visible = false +end + +local function restart() + dt.gui.libs["gpx_exporter"].visible = true +end + +gpx.widget = dt.new_widget("box") +{ + orientation = "vertical", + dt.new_widget("button") + { + label = _("export"), + tooltip = _("export gpx file"), + clicked_callback = create_gpx_file + }, + dt.new_widget("box") + { + orientation = "horizontal", + dt.new_widget("label") + { + label = _("file:"), + }, + path_entry + }, +} + + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not gpx.event_registered then + dt.register_event( + "gpx_export", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + gpx.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 diff --git a/contrib/harmonic_armature_guide.lua b/contrib/harmonic_armature_guide.lua new file mode 100644 index 00000000..ffe03a29 --- /dev/null +++ b/contrib/harmonic_armature_guide.lua @@ -0,0 +1,97 @@ +--[[ + harmonic artmature guide for darktable + + copyright (c) 2021 Hubert Kowalski + + 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 . +]] + +--[[ +HARMONIC ARMATURE GUIDE +Harmonic Armature (also known as 14 line armature) + +INSTALLATION +* copy this file in $CONFIGDIR/lua/contrib where CONFIGDIR is your darktable configuration directory +* add the following line in the file $CONFIGDIR/luarc + require "contrib/harmonic_armature_guide" + +USAGE +* when using guides, select "harmonic armature" as guide +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local gettext = dt.gettext.gettext + +du.check_min_api_version("2.0.0", "harmonic_armature_guide") + +local function _(msgid) + return gettext(msgid) +end + +local script_data = {} + +script_data.metadata = { + name = _("harmonic armature guide"), + purpose = _("harmonic artmature guide"), + author = "Hubert Kowalski", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/harmonic_armature_guide" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +dt.guides.register_guide("harmonic armature", +-- draw +function(cairo, x, y, width, height, zoom_scale) + cairo:save() + + cairo:translate(x, y) + cairo:scale(width, height) + + cairo:move_to(0,0) + cairo:line_to(1, 0.5) + cairo:line_to(0.5, 1) + cairo:line_to(0,0) + cairo:line_to(1, 1) + cairo:line_to(0.5, 0) + cairo:line_to(0, 0.5) + cairo:line_to(1, 1) + + cairo:move_to(1, 0) + cairo:line_to(0, 0.5) + cairo:line_to(0.5, 1) + cairo:line_to(1, 0) + cairo:line_to(0, 1) + cairo:line_to(0.5, 0) + cairo:line_to(1, 0.5) + cairo:line_to(0, 1) + + cairo:restore() +end, +-- gui +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 diff --git a/contrib/hif_group_leader.lua b/contrib/hif_group_leader.lua new file mode 100644 index 00000000..412eea07 --- /dev/null +++ b/contrib/hif_group_leader.lua @@ -0,0 +1,200 @@ +--[[ + + hif_group_leader.lua - Make hif image group leader + + Copyright (C) 2024 Bill Ferguson . + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +]] +--[[ + hif_group_leader - Make hif image group leader + + After a film roll is imported, check for RAW-JPG image groups + and make the JPG image the group leader. This is on by default + but can be disabled in preferences. + + Shortcuts are included to filter existing collections or + selections of images and make the hif the group leader. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + None + + USAGE + Start script from script_manager + Assign keys to the shortcuts + + BUGS, COMMENTS, SUGGESTIONS + Bill Ferguson + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "hif_group_leader" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("7.0.0", MODULE) + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- I 1 8 N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- S C R I P T M A N A G E R I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local script_data = {} + +script_data.metadata = { + name = _("HIF group leader"), + purpose = _("make hif image group leader"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/hif_group_leader" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- P R E F E R E N C E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.preferences.register(MODULE, "on_import", "bool", _("make hif group leader on import"), _("automatically make the hif file the group leader when raw + hif are imported"), true) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local jgloi = {} +jgloi.images = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function toggle_global_toolbox_grouping() + dt.gui.libs.global_toolbox.grouping = false + dt.gui.libs.global_toolbox.grouping = true +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N P R O G R A M +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function make_hif_group_leader(images) + -- if the image is part of a group, make it the leader + for _, image in ipairs(images) do + if #image:get_group_members() > 1 then + image:make_group_leader() + end + end + if dt.gui.libs.global_toolbox.grouping then + -- toggle the grouping to make the new leader show + toggle_global_toolbox_grouping() + end +end + +local function make_existing_hif_group_leader(images) + for _, image in ipairs(images) do + if string.lower(df.get_filetype(image.filename)) == "hif" then + if #image:get_group_members() > 1 then + image:make_group_leader() + end + end + end + if dt.gui.libs.global_toolbox.grouping then + -- toggle the grouping to make the new leader show + toggle_global_toolbox_grouping() + end +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + if dt.preferences.read(MODULE, "on_import", "bool") then + dt.destroy_event(MODULE, "post-import-film") + dt.destroy_event(MODULE, "post-import-image") + end + dt.destroy_event(MODULE .. "_collect", "shortcut") + dt.destroy_event(MODULE .. "_select", "shortcut") +end + +script_data.destroy = destroy + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +if dt.preferences.read(MODULE, "on_import", "bool") then + dt.register_event(MODULE, "post-import-film", + function(event, film_roll) + -- ignore the film roll, it contains all the images, not just the imported + local images = jgloi.images + if #images > 0 then + jgloi.images = {} + make_hif_group_leader(images) + end + end + ) + + dt.register_event(MODULE, "post-import-image", + function(event, image) + if string.lower(df.get_filetype(image.filename)) == "hif" then + table.insert(jgloi.images, image) + end + end + ) +end + +dt.register_event(MODULE .. "_collect", "shortcut", + function(event, shortcut) + -- ignore the film roll, it contains all the images, not just the imported + local images = dt.collection + make_existing_hif_group_leader(images) + end, + _("make hif group leader for collection") +) + +dt.register_event(MODULE .. "_select", "shortcut", + function(event, shortcut) + local images = dt.gui.selection() + make_existing_hif_group_leader(images) + end, + _("make hif group leader for selection") +) + +return script_data \ No newline at end of file diff --git a/contrib/hugin.lua b/contrib/hugin.lua index d69a8ad7..46fbd90c 100644 --- a/contrib/hugin.lua +++ b/contrib/hugin.lua @@ -28,85 +28,221 @@ ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT * hugin USAGE -* require this file from your main luarc config file. +* require this file from your main luarc config file +* set the hugin tool paths (on some platforms) +* if hugin gui mode is used, save the final result in the tmp directory with the first file name and _pano as suffix for the image to be automatically imported to DT afterwards This plugin will add a new storage option and calls hugin after export. ]] local dt = require "darktable" +local du = require "lib/dtutils" local df = require "lib/dtutils.file" -require "official/yield" -local gettext = dt.gettext +local log = require "lib/dtutils.log" +local dtsys = require "lib/dtutils.system" +local gettext = dt.gettext.gettext --- works with darktable API version from 2.0.0 to 5.0.0 -dt.configuration.check_version(...,{2,0,0},{3,0,0},{4,0,0},{5,0,0}) +local namespace = 'module_hugin' +local user_pref_str = 'prefer_gui' +local user_prefer_gui = dt.preferences.read(namespace, user_pref_str, "bool") +log.msg(log.info, "user_prefer_gui set to ", user_prefer_gui) +local hugin_widget = nil +local exec_widget = nil +local executable_table = {"hugin", "hugin_executor", "pto_gen"} --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("hugin",dt.configuration.config_dir.."/lua/locale/") +-- get the proper path quoting quote +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.dgettext("hugin", 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 + +local function user_preference_changed(widget) + user_prefer_gui = widget.value + dt.preferences.write(namespace, user_pref_str, "bool", user_prefer_gui) end local function show_status(storage, image, format, filename, number, total, high_quality, extra_data) - dt.print("Export to Hugin "..tostring(number).."/"..tostring(total)) + dt.print(string.format(_("exporting to hugin: %d / %d"), number, total)) end local function create_panorama(storage, image_table, extra_data) --finalize - if not df.check_if_bin_exists("hugin") then - dt.print_error(_("hugin not found")) - return - end - -- Since Hugin 2015.0.0 hugin provides a command line tool to start the assistant -- http://wiki.panotools.org/Hugin_executor -- We need pto_gen to create pto file for hugin_executor -- http://hugin.sourceforge.net/docs/manual/Pto_gen.html - local hugin_executor = false - if (df.check_if_bin_exists("hugin_executor") and df.check_if_bin_exists("pto_gen")) then - hugin_executor = true + -- save the current log level and set log level to + -- log.error for normal operations + -- log.info for more insight about what is happening + -- log.debug for even more information + + local saved_log_level = log.log_level() + log.log_level(log.error) + + local hugin = df.check_if_bin_exists("hugin") + log.msg(log.info, "hugin set to ", hugin) + local hugin_executor = df.check_if_bin_exists("hugin_executor") + log.msg(log.info, "hugin_executor set to ", hugin_executor) + local pto_gen = df.check_if_bin_exists("pto_gen") + log.msg(log.info, "pto_gen set to ", pto_gen) + + local gui_available = false + if hugin then + gui_available = true + else + dt.print(_("hugin is not found, did you set the path?")) + log.msg(log.error, "hugin executable not found. Check if the executable is installed.") + log.msg(log.error, "If the executable is installed, check that the path is set correctly.") + return + end + + local cmd_line_available = false + if hugin_executor and pto_gen then + cmd_line_available = true end -- list of exported images - local img_list + local img_list = "" + local img_set = {} -- reset and create image list - img_list = "" - - for _,v in pairs(image_table) do - img_list = img_list ..v.. " " + for k,v in pairs(image_table) do + log.msg(log.debug, "k is ", k, " and v is ", v) + img_list = img_list .. PQ .. v .. PQ .. ' ' -- surround the filename with single quotes to handle spaces + table.insert(img_set, k) end - dt.print(_("Will try to stitch now")) + log.msg(log.info, "img_list is ", img_list) - local huginStartCommand - if (hugin_executor) then - huginStartCommand = "pto_gen "..img_list.." -o "..dt.configuration.tmp_dir.."/project.pto" - dt.print(_("Creating pto file")) - dt.control.execute( huginStartCommand) + -- use first file as basename for output file + table.sort(img_set, function(a,b) return a.filename. + Copyright (C) 2016,2017 Holger Klemm + + 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 . +]] +--[[ + image_stack - export a stack of images and process them, returning the result + + This script provides another storage (export target) for darktable. Selected + images are exported in the specified format to temporary storage. The images are aligned + if the user requests it. When the images are ready, imagemagick is launched and uses + the selected evaluate-sequence operator to process the images. The output file is written + to a filename representing the imput files in the format specified by the user. The resulting + image is imported into the film roll. The source images can be tagged as part of the file + creation so that a user can later find the contributing images. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + * align_image_stack - http://www.hugin.org + * imagemagick - http://www.imagemagick.org + + USAGE + * require this script from your main lua file + * select the images to process with image_stack + * in the export dialog select "image stack" and select the format and bit depth for the + exported image + * Select whether the images need to be aligned. + * Select the stack operator + * Select the output format + * Select whether to tag the source images used to create the resulting file + * Specify executable locations if necessary + * Press "export" + * The resulting image will be imported + + NOTES + Mean is a fairly quick operation. On my machine (i7-6800K, 16G) it takes a few seconds. Median, on the other hand + takes approximately 10x longer to complete. Processing 10 and 12 image stacks took over a minute. I didn't test all + the other functions, but the ones I did fell between Mean and Median performance wise. + + BUGS, COMMENTS, SUGGESTIONS + * Send to Bill Ferguson, wpferguson@gmail.com + + CHANGES + + THANKS + * Thanks to Pat David and his blog entry on blending images, https://patdavid.net/2013/05/noise-removal-in-photos-with-median_6.html +]] + +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 job = nil + +-- path separator constant +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +-- works with LUA API version 5.0.0 +du.check_min_api_version("7.0.0", "image_stack") + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("image stack"), + purpose = _("process a stack of images"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/image_stack" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- GUI definitions +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +local label_align_options= dt.new_widget("section_label"){ + label = _('image align options') +} + +local label_image_stack_options = dt.new_widget("section_label"){ + label = _("image stack options") +} + +local label_executable_locations = dt.new_widget("section_label"){ + label = _("executable locations") +} + +if not(dt.preferences.read("align_image_stack", "initialized", "bool")) then + dt.preferences.write("align_image_stack", "def_radial_distortion", "bool", false) + dt.preferences.write("align_image_stack", "def_optimize_field", "bool", false) + dt.preferences.write("align_image_stack", "def_optimize_image_center", "bool", true) + dt.preferences.write("align_image_stack", "def_auto_crop", "bool", true) + dt.preferences.write("align_image_stack", "def_distortion", "bool", true) + dt.preferences.write("align_image_stack", "def_grid_size", "integer", 5) + dt.preferences.write("align_image_stack", "def_control_points", "integer", 8) + dt.preferences.write("align_image_stack", "def_control_points_remove", "integer", 3) + dt.preferences.write("align_image_stack", "def_correlation", "integer", 9) + dt.preferences.write("align_image_stack", "initialized", "bool", true) +end + +if not(dt.preferences.read("image_stack", "initialized", "bool")) then + dt.preferences.write("image_stack", "align_images", "bool", true) + dt.preferences.write("image_stack", "stack_function", "integer", 2) + dt.preferences.write("image_stack", "output_format", "integer", 6) + dt.preferences.write("image_stack", "tag_images", "bool", true) + dt.preferences.write("image_stack", "initialized", "bool", true) +end + +local chkbtn_will_align = dt.new_widget("check_button"){ + label = _('perform image alignment'), + value = dt.preferences.read("image_stack", "align_images", "bool"), + tooltip = _('align the image stack before processing') +} + +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'), +} + +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.'), +} + +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.'), +} + +local chkbtn_auto_crop = dt.new_widget("check_button"){ + label = _('auto crop the image'), + value = dt.preferences.read("align_image_stack", "def_auto_crop", "bool"), + tooltip =_('auto crop the image to the area covered by all images.'), +} + +local chkbtn_distortion = dt.new_widget("check_button"){ + label = _('load distortion from lens database'), + value = dt.preferences.read("align_image_stack", "def_distortion", "bool"), + tooltip =_('try to load distortion information from lens database'), +} + +local cmbx_grid_size = dt.new_widget("combobox"){ + label = _('image grid size'), + tooltip =_('break image into a rectangular grid \nand attempt to find num control points in each section.\ndefault: (5x5)'), + value = dt.preferences.read("align_image_stack", "def_grid_size", "integer"), --5 + "1", "2", "3","4","5","6","7","8","9", + reset_callback = function(self) + self.value = dt.preferences.read("align_image_stack", "def_grid_size", "integer") + end +} + +local cmbx_control_points = dt.new_widget("combobox"){ + label = _('control points/grid'), + tooltip =_('number of control points (per grid, see option -g) \nto create between adjacent images \ndefault: (8).'), + value = dt.preferences.read("align_image_stack", "def_control_points", "integer"), --8, "1", "2", "3","4","5","6","7","8","9", + "1", "2", "3","4","5","6","7","8","9", + reset_callback = function(self) + self.value = dt.preferences.read("align_image_stack", "def_control_points", "integer") + end +} + +local cmbx_control_points_remove = dt.new_widget("combobox"){ + label = _('remove control points with error'), + tooltip =_('remove all control points with an error higher \nthan num pixels \ndefault: (3)'), + value = dt.preferences.read("align_image_stack", "def_control_points_remove", "integer"), --3, "1", "2", "3","4","5","6","7","8","9", + "1", "2", "3","4","5","6","7","8","9", + reset_callback = function(self) + self.value = dt.preferences.read("align_image_stack", "def_control_points_remove", "integer") + end +} + +local cmbx_correlation = dt.new_widget("combobox"){ + label = _('correlation threshold for control points'), + tooltip =_('correlation threshold for identifying \ncontrol points \ndefault: (0.9).'), + value = dt.preferences.read("align_image_stack", "def_correlation", "integer"), --9, "0,1", "0,2", "0,3","0,4","0,5","0,6","0,7","0,8","0,9", + "0.1", "0.2", "0.3","0.4","0.5","0.6","0.7","0.8","0.9","1.0", + reset_callback = function(self) + self.value = dt.preferences.read("align_image_stack", "def_correlation", "integer") + end +} + +local cmbx_stack_function = dt.new_widget("combobox"){ + label = _("select stack function"), + tooltip = _("select function to be \napplied to image stack"), + value = dt.preferences.read("image_stack", "stack_function", "integer"), "Mean", "Median", "Abs", "Add", "And", "Divide", "Max", "Min", + "Or", "Subtract", "Sum", "Xor", + reset_callback = function(self) + self.value = dt.preferences.read("image_stack", "stack_function", "integer") + end +} + +local cmbx_output_format = dt.new_widget("combobox"){ + label = _("select output format"), + tooltip = _("choose the format for the resulting image"), + value = dt.preferences.read("image_stack", "output_format", "integer"), "EXR", "JPG", "PNG", "PNM", "PPM", "TIF", + reset_callback = function(self) + self.value = dt.preferences.read("image_stack", "output_format", "integer") + end +} + +local chkbtn_tag_source_file = dt.new_widget("check_button"){ + label = _("tag source images used?"), + value = dt.preferences.read("image_stack", "tag_images", "bool"), + tooltip = _("tag the source images used to create the output file?") +} + +local image_stack_widget = dt.new_widget("box"){ + orientation = "vertical", + label_align_options, + chkbtn_will_align, + chkbtn_radial_distortion, + chkbtn_optimize_field, + chkbtn_optimize_image_center, + chkbtn_auto_crop, + chkbtn_distortion, + cmbx_grid_size, + cmbx_control_points, + cmbx_control_points_remove, + cmbx_correlation, + dt.new_widget("separator"){}, + dt.new_widget("separator"){}, + label_image_stack_options, + cmbx_stack_function, + cmbx_output_format, + chkbtn_tag_source_file, +} + +local executables = {"align_image_stack", "convert"} + +if dt.configuration.running_os ~= "linux" then + image_stack_widget[#image_stack_widget + 1] = df.executable_path_widget(executables) +end + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- local functions +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +local function show_status(storage, image, format, filename, number, total, high_quality, extra_data) + dt.print(string.format(_("export image %i/%i"), number, total)) +end + +-- read the gui and populate the align_image_stack arguments + +local function get_align_image_stack_arguments() + + --Setup Align Image Stack Arguments-- + local align_args = "" + if (chkbtn_radial_distortion.value) then align_args = align_args .. " -d" end + if (chkbtn_optimize_field.value) then align_args = align_args .. " -m" end + if (chkbtn_optimize_image_center.value) then align_args = align_args .. " -i" end + if (chkbtn_auto_crop.value) then align_args = align_args .. " -C" end + if (chkbtn_distortion.value) then align_args = align_args .. " --distortion" end + + dt.preferences.write("align_image_stack", "def_radial_distortion", "bool", chkbtn_radial_distortion.value) + dt.preferences.write("align_image_stack", "def_optimize_field", "bool", chkbtn_optimize_field.value) + dt.preferences.write("align_image_stack", "def_optimize_image_center", "bool", chkbtn_optimize_image_center.value) + dt.preferences.write("align_image_stack", "def_auto_crop", "bool", chkbtn_auto_crop.value) + dt.preferences.write("align_image_stack", "def_distortion", "bool", chkbtn_distortion.value) + + align_args = align_args.." -g "..cmbx_grid_size.value + align_args = align_args.." -c "..cmbx_control_points.value + align_args = align_args.." -t "..cmbx_control_points_remove.value + align_args = align_args.." --corr="..cmbx_correlation.value + + dt.preferences.write("align_image_stack", "def_grid_size", "integer", cmbx_grid_size.selected) + dt.preferences.write("align_image_stack", "def_control_points", "integer", cmbx_control_points.selected) + dt.preferences.write("align_image_stack", "def_control_points_remove", "integer", cmbx_control_points_remove.selected) + dt.preferences.write("align_image_stack", "def_correlation", "integer", cmbx_correlation.selected) + + if (dt.preferences.read("align_image_stack", "align_use_gpu", "bool")) then align_args = align_args .. " --gpu" end + + align_args = align_args .. " -a " .. dt.configuration.tmp_dir .. "/aligned_ " + + return align_args +end + +-- extract, and sanitize, an image list from the supplied image table + +local function extract_image_list(image_table) + local img_list = "" + local result = {} + + for img,expimg in pairs(image_table) do + table.insert(result, expimg) + end + table.sort(result) + for _,exp_img in ipairs(result) do + img_list = img_list .. " " .. df.sanitize_filename(exp_img) + end + return img_list, #result +end + +-- don't leave files laying around the operating system + +local function cleanup(img_list) + dt.print_log("image list is " .. img_list) + files = du.split(img_list, " ") + for _,f in ipairs(files) do + f = string.gsub(f, '[\'\"]', "") + os.remove(f) + end +end + +-- List files based on a search pattern. This is cross platform compatible +-- but the windows version is recursive in order to get ls type listings. +-- Normally this shouldn't be a problem, but if you use this code just beware. +-- If you want to do it non recursively, then remove the /s argument from dir +-- and grab the path component from the search string and prepend it to the files +-- found. + +local function list_files(search_string) + local ls = "ls " + local files = {} + local dir_path = nil + local count = 1 + + if dt.configuration.running_os == "windows" then + ls = "dir /b/s " + search_string = string.gsub(search_string, "/", "\\\\") + end + + local f = io.popen(ls .. search_string) + if f then + local found_file = f:read() + while found_file do + files[count] = found_file + count = count + 1 + found_file = f:read() + end + f:close() + end + return files +end + +-- create a filename from a multi image set. The image list is sorted, then +-- combined with first and last if more than 3 images or a - separated list +-- if three images or less. + +local function make_output_filename(image_table) + local images = {} + local cnt = 1 + local max_distinct_names = 3 + local name_separator = "-" + local outputFileName = nil + local result = {} + + for img,expimg in pairs(image_table) do + table.insert(result, expimg) + end + table.sort(result) + for _,img in pairs(result) do + images[cnt] = df.get_basename(img) + cnt = cnt + 1 + end + + cnt = cnt - 1 + + if cnt > 1 then + if cnt > max_distinct_names then + -- take the first and last + outputFileName = images[1] .. name_separator .. images[cnt] + else + -- join them + outputFileName = du.join(images, name_separator) + end + else + -- return the single name + outputFileName = images[cnt] + end + + return outputFileName +end + +-- get the path where the collection is stored + +local function extract_collection_path(image_table) + local collection_path = nil + for i,_ in pairs(image_table) do + collection_path = i.path + break + end + return collection_path +end + +-- copy an images database attributes to another image. This only +-- copies what the database knows, not the actual exif data in the +-- image itself. + +local function copy_image_attributes(from, to, ...) + local args = {...} + if #args == 0 then + args[1] = "all" + end + if args[1] == "all" then + args[1] = "rating" + args[2] = "colors" + args[3] = "exif" + args[4] = "meta" + args[5] = "GPS" + end + for _,arg in ipairs(args) do + if arg == "rating" then + to.rating = from.rating + elseif arg == "colors" then + to.red = from.red + to.blue = from.blue + to.green = from.green + to.yellow = from.yellow + to.purple = from.purple + elseif arg == "exif" then + to.exif_maker = from.exif_maker + to.exif_model = from.exif_model + to.exif_lens = from.exif_lens + to.exif_aperture = from.exif_aperture + to.exif_exposure = from.exif_exposure + to.exif_focal_length = from.exif_focal_length + to.exif_iso = from.exif_iso + to.exif_datetime_taken = from.exif_datetime_taken + to.exif_focus_distance = from.exif_focus_distance + to.exif_crop = from.exif_crop + elseif arg == "GPS" then + to.elevation = from.elevation + to.longitude = from.longitude + to.latitude = from.latitude + elseif arg == "meta" then + to.publisher = from.publisher + to.title = from.title + to.creator = from.creator + to.rights = from.rights + to.description = from.description + else + dt.print_error("Unrecognized option to copy_image_attributes: " .. arg) + end + end +end + +local function stop_job() + job.valid = false +end + +local function destroy() + dt.destroy_storage("module_image_stack") +end + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- main program +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +local function image_stack(storage, image_table, extra_data) + + local will_align = chkbtn_will_align.value + local img_list, image_count = extract_image_list(image_table) + local tmp_dir = dt.configuration.tmp_dir .. PS + local stack_function = cmbx_stack_function.value + local output_format = cmbx_output_format.value + local tag_source = chkbtn_tag_source_file.value + local tasks = 3 + + if will_align then + tasks = tasks + 1 + end + + if tag_source then + tasks = tasks + 1 + end + + local percent_step = 1 / tasks + + -- update preferences + dt.preferences.write("image_stack", "align_images", "bool", will_align) + dt.preferences.write("image_stack", "stack_function", "integer", cmbx_stack_function.selected) + dt.preferences.write("image_stack", "output_format", "integer", cmbx_output_format.selected) + + 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") + return + end + + job = dt.gui.create_job(_("image stack"), true, stop_job) + job.percent = job.percent + percent_step + + -- align images if requested + if will_align then + local align_image_stack_executable = df.check_if_bin_exists("align_image_stack") + if align_image_stack_executable then + local align_args = get_align_image_stack_arguments() + local align_image_stack_command = align_image_stack_executable .. align_args .. " " .. img_list + dt.print_log(align_image_stack_command) + dt.print(_("aligning images...")) + local result = dtsys.external_command(align_image_stack_command) + if result == 0 then + dt.print(_("images aligned")) + local files = list_files(tmp_dir .. "aligned_*") + cleanup(img_list) + img_list = extract_image_list(files) + job.percent = job.percent + percent_step + else + 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") + cleanup(img_list) + return + end + end + + -- stack images + local output_filename = tmp_dir .. make_output_filename(image_table) .. "." .. string.lower(output_format) + local convert_arguments = img_list .. " -evaluate-sequence " .. stack_function .. " " .. df.sanitize_filename(output_filename) + local convert_executable = df.check_if_bin_exists("convert") + -- tif images exported from darktable have private tiff tags embedded in them and convert complains about them + 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(_("processing image stack")) + local result = dtsys.external_command(convert_command) + if result == 0 then + dt.print(_("image stack processed")) + cleanup(img_list) + job.percent = job.percent + percent_step + + -- import image + dt.print(_("importing result")) + local film_roll_path = extract_collection_path(image_table) + 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")) + dt.tags.attach(created_tag, imported_image) + -- all the images are the same except for time, so just copy the attributes + -- from the first + for img,_ in pairs(image_table) do + copy_image_attributes(img, imported_image) + break + end + job.percent = job.percent + percent_step + + -- tag images if requested + + if tag_source then + dt.print(_("tagging source images")) + 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 + job.percent = job.percent + percent_step + end + else + dt.print(_("ERROR: image stack processing failed")) + cleanup(img_list) + end + else + dt.print(_("ERROR: convert executable not found")) + dt.print_error("convert executable not found") + cleanup(img_list) + end + job.valid = false +end + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- darktable integration +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +dt.preferences.register("align_image_stack", "align_use_gpu", -- name + "bool", -- type + _('align image stack: use GPU for remapping'), -- label + _('set the GPU remapping for image align'), -- tooltip + false) + +dt.register_storage("module_image_stack", _("image stack"), show_status, image_stack, nil, nil, image_stack_widget) + +script_data.destroy = destroy + +return script_data diff --git a/contrib/image_time.lua b/contrib/image_time.lua new file mode 100644 index 00000000..c0971306 --- /dev/null +++ b/contrib/image_time.lua @@ -0,0 +1,593 @@ +--[[ + + image_time.lua - synchronize image time for images shot with different cameras + + Copyright (C) 2019, 2020 Bill Ferguson . + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +]] +--[[ + image_time - non-destructively modify the image time + + DESCRIPTION + + image_time non destructively adjusts image times by modifying the + database image exif_datetime_taken field. There are 4 modes: adjust time, + set time, synchronize time, and reset time. + + ADJUST TIME + + adjust time mode lets you chose an offset in terms of years, months, + days, hours, minutes, and seconds. The adjustment can be added or + subtracted. + + WARNING: When adding and subtracting months the result will usually + be what is expected unless the time being adjusted is at the end of + the month. This is because a month is a variable amount of time that + can be 28, 29, 30 or 31 days depending on the month. Example: It's + March 31st and I subtract a month which not sets the time to February + 31st. When that gets set to a valid time, then the date changes to + March 3rd. + + SET TIME + + set time mode allows you to pick a date and time and set the image + time accordingly. Fields may be left out. This is useful when + importing scanned images that don't have an embedded date. + + SYNCHRONIZE TIME + + I recently purchased a 7DmkII to replace my aging 7D. My 7D was still + serviceable, so I bought a remote control and figured I'd try shooting + events from 2 different perspectives. I didn't think to synchonize the + time between the 2 cameras, so when I loaded the images and sorted by + time it was a disaster. I hacked a script together with hard coded values + to adjust the exif_datetime_taken value in the database for the 7D images + so that everything sorted properly. I've tried shooting with 2 cameras + several times since that first attempt. I've gotten better at getting the + camera times close, but still haven't managed to get them to sync. So I + decided to think the problem through and write a proper script to take + care of the problem. + + RESET TIME + + Select the images and click reset. + + USAGE + + ADJUST TIME + + Change the year, month, day, hour, minute, second dropdowns to the amount + of change desired. Select add or subtract. Select the images. Click + adjust. + + SET TIME + + Set the time fields to the desired time. Select the images to change. Click + set. + + SYNCHRONIZE TIME + + Select 2 images, one from each camera, of the same moment in time. Click + the Calculate button to calculate the time difference. The difference is + displayed in the difference entry. You can manually adjust it by changing + the value if necessary. + + Select the images that need their time adjusted. Determine which way to adjust + adjust the time (add or subtract) and select the appropriate choice. + + If the image times get messed up and you just want to start over, select reset time + from the mode and reset the image times. + + RESET TIME + + Select the images and click reset. + + ADDITIONAL SOFTWARE REQUIRED + * exiv2 + + BUGS, COMMENTS, SUGGESTIONS + * Send to 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 gettext = dt.gettext.gettext + +local img_time = {} +img_time.module_installed = false +img_time.event_registered = false + +du.check_min_api_version("7.0.0", "image_time") + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("image time"), + purpose = _("synchronize image time for images shot with different cameras or adjust or set image time"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/image_time" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + + + +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) + local yr,mo,dy,h,m,s = string.match(exiftime, "(%d-):(%d-):(%d-) (%d-):(%d-):(%d+)") + return(os.time{year=yr, month=mo, day=dy, hour=h, min=m, sec=s}) +end + +-- function to convert from systime to exif time +local function systime2exiftime(systime) + local t = os.date("*t", systime) + return(string.format("%4d:%02d:%02d %02d:%02d:%02d", t.year, t.month, t.day, t.hour, t.min, t.sec)) +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" + return(y .. ":" .. mo .. ":" .. d .. " " .. h .. ":" .. m .. ":" .. s) +end + +local function exiftime2vars(exiftime) + return string.match(exiftime, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)") +end + +local function calc_time_difference(image1, image2) + return math.abs(exiftime2systime(image1.exif_datetime_taken) - exiftime2systime(image2.exif_datetime_taken)) +end + +local function adjust_image_time(image, difference) + image.exif_datetime_taken = systime2exiftime(exiftime2systime(image.exif_datetime_taken) + difference) + return +end + +local function calculate_difference(images) + if #images == 2 then + 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")) + end +end + +local function synchronize_times(images, difference) + for _, image in ipairs(images) do + adjust_image_time(image, difference) + end +end + +local function synchronize_time(images) + local sign = 1 + if img_time.sdir.value == _("subtract") then + sign = -1 + end + synchronize_times(images, tonumber(img_time.diff_entry.text) * sign) +end + +local function add_time(images) + synchronize_times(images, tonumber(img_time.diff_entry.text)) +end + +local function year_month2months(year, month) + year_months = tonumber(year) and year * 12 or 0 + local months = tonumber(month) and tonumber(month) or 0 + + return year_months + months +end + +local function months2year_month(months) + dt.print_log("months is " .. months) + local year = math.floor(months / 12) + local month = months - (year * 12) + + return year, month +end + +local function get_image_taken_time(image) + -- get original image time + local datetime = nil + + local exiv2 = df.check_if_bin_exists("exiv2") + if exiv2 then + 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 + datetime = string.match(line, "(%d-:%d-:%d- %d-:%d-:%d+)") + end + end + p:close() + end + else + dt.print(_("unable to detect exiv2")) + datetime = ERROR + end + return datetime +end + +local function _get_windows_image_file_creation_time(image) + local datetime = nil + 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 + end + p:close() + else + dt.print(string.format(_("unable to get information for %s"), 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 " .. image.path .. PS .. image.filename) + if p then + for line in p:lines() do + if string.match(line, ds.sanitize_lua(image.filename)) then + datetime = vars2exiftime(string.match(line, "(%d+)%-(%d-)%-(%d-) (%d-):(%d-):(%d+).")) + end + end + p:close() + else + dt.print(string.format(_("unable to get information for %s"), image.filename)) + datetime = ERROR + end + return datetime +end + +local function get_image_file_creation_time(image) + -- no exif time in the image file so get the creation time + local datetime = nil + if dt.configuration.running_os == "windows" then + datetime = _get_windows_image_file_creation_time(image) + else + datetime = _get_nix_image_file_creation_time(image) + end + return datetime +end + +local function get_original_image_time(image) + local image_time = image.exif_datetime_taken + local reset_time = nil + + reset_time = get_image_taken_time(image) + + if reset_time then + if reset_time == ERROR then + return image_time + else + return reset_time + end + else + reset_time = get_image_file_creation_time(image) + + if reset_time then + if reset_time == ERROR then + return image_time + else + return reset_time + end + end + end +end + +local function reset_time(images) + if #images > 0 then + for _, image in ipairs(images) do + image.exif_datetime_taken = get_original_image_time(image) + end + else + dt.print_error("reset time: no images selected") + dt.print(_("please select the images that need their time reset")) + end +end + +local function adjust_time(images) + local SEC_MIN = 60 + local SEC_HR = SEC_MIN * 60 + local SEC_DY = SEC_HR * 24 + + local offset = nil + local sign = 1 + + if #images < 1 then + dt.print(_("please select some images and try again")) + return + end + + if img_time.adir.value == _("subtract") then + sign = -1 + end + + for _, image in ipairs(images) do + local y, mo, d, h, m, s = exiftime2vars(image.exif_datetime_taken) + local image_months = year_month2months(y, mo) + local months_diff = year_month2months(img_time.ayr.value, img_time.amo.value) + y, mo = months2year_month(image_months + (months_diff * sign)) + local exif_new = vars2exiftime(y, mo, d, h, m, s) + offset = img_time.ady.value * SEC_DY + offset = offset + img_time.ahr.value * SEC_HR + offset = offset + img_time.amn.value * SEC_MIN + offset = offset + img_time.asc.value + offset = offset * sign + image.exif_datetime_taken = systime2exiftime(exiftime2systime(exif_new) + offset) + end +end + +local function set_time(images) + if #images < 1 then + dt.print(_("please select some images and try again")) + return + end + + local y = img_time.syr.value + local mo = img_time.smo.value + local d = img_time.sdy.value + local h = img_time.shr.value + local m = img_time.smn.value + local s = img_time.ssc.value + + for _, image in ipairs(images) do + image.exif_datetime_taken = vars2exiftime(y, mo, d, h, m, s) + end +end + +local function seq(first, last) + local result = {} + + local num = first + + while num <= last do + table.insert(result, num) + num = num + 1 + end + + return table.unpack(result) +end + +local function reset_widgets() + dt.print_log("took the reset function") + img_time.ayr.selected = 1 + img_time.amo.selected = 1 + img_time.ady.selected = 1 + img_time.ahr.selected = 1 + img_time.ayr.selected = 1 + img_time.amn.selected = 1 + img_time.asc.selected = 1 + img_time.adir.selected = 1 + img_time.syr.selected = #img_time.syr + img_time.smo.selected = 1 + img_time.sdy.selected = 1 + img_time.shr.selected = 1 + img_time.smn.selected = 1 + img_time.ssc.selected = 1 + img_time.adir.selected = 1 +end + +local function install_module() + if not img_time.module_installed then + dt.register_lib( + "image_time", -- Module name + _("image time"), -- Visible name + true, -- expandable + true, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers + img_time.widget, + nil,-- view_enter + nil -- view_leave + ) + img_time.module_installed = true + end +end + +local function destroy() + dt.gui.libs["image_time"].visible = false +end + +local function restart() + dt.gui.libs["image_time"].visible = true +end + +-- widgets + +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}, + {"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}, + {"adir", "combobox", _("add/subtract"), _("add or subtract time"), {_("add"), _("subtract")}, 1}, + {"syr", "combobox", _("year"), _("year to set, 1900 - now"), {" ", seq(1900,os.date("*t", os.time()).year)}, 1}, + {"smo", "combobox", _("month"), _("month to set, 1-12"), {" ", seq(1,12)}, 1}, + {"sdy", "combobox", _("day"), _("day to set, 1-31"), {" ", seq(1,31)}, 1}, + {"shr", "combobox", _("hour"), _("hour to set, 0-23"), {" ", seq(0,23)}, 1}, + {"smn", "combobox", _("minute"), _("minutes to set, 0-59"), {" ", seq(0, 59)}, 1}, + {"ssc", "combobox", _("seconds"), _("seconds to set, 0-59"), {" ", seq(0,59)}, 1}, + {"sdir", "combobox", _("add/subtract"), _("add or subtract time"), {_("add"), _("subtract")}, 1}, +} + +for _, widget in ipairs(img_time.widgets) do + img_time[widget[1]] = dt.new_widget(widget[2]){ + label = widget[3], + tooltip = widget[4], + selected = widget[6], + table.unpack(widget[5]) + } +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"), + text = "", +} + +img_time.calc_btn = dt.new_widget("button"){ + label = _("calculate"), + tooltip = _("calculate time difference between 2 images"), + clicked_callback = function() + calculate_difference(dt.gui.action_images) + end +} + +img_time.btn = dt.new_widget("button"){ + label = _("synchronize image times"), + tooltip = _("apply the time difference from selected images"), + sensitive = false, + clicked_callback = function() + synchronize_time(dt.gui.action_images) + end +} + +img_time.stack = dt.new_widget("stack"){ + dt.new_widget("box"){ + orientation = "vertical", + dt.new_widget("label"){label = _("adjust time")}, + dt.new_widget("section_label"){label = _("days, months, years")}, + img_time.ady, + img_time.amo, + img_time.ayr, + dt.new_widget("section_label"){label = _("hours, minutes, seconds")}, + img_time.ahr, + img_time.amn, + img_time.asc, + dt.new_widget("section_label"){label = _("adjustment direction")}, + img_time.adir, + dt.new_widget("button"){ + label = _("adjust"), + clicked_callback = function() + adjust_time(dt.gui.action_images) + end + } + }, + dt.new_widget("box"){ + orientation = "vertical", + dt.new_widget("label"){label = _("set time")}, + dt.new_widget("section_label"){label = _("date:")}, + img_time.sdy, + img_time.smo, + img_time.syr, + dt.new_widget("section_label"){label = _("time:")}, + img_time.shr, + img_time.smn, + img_time.ssc, + dt.new_widget("button"){ + label = _("set"), + clicked_callback = function() + set_time(dt.gui.action_images) + end + } + }, + dt.new_widget("box"){ + orientation = "vertical", + dt.new_widget("label"){label = _("synchronize image time")}, + dt.new_widget("section_label"){label = _("calculate difference between images")}, + img_time.diff_entry, + img_time.calc_btn, + dt.new_widget("section_label"){label = _("apply difference")}, + img_time.sdir, + img_time.btn, + }, + dt.new_widget("box"){ + orientation = "vertical", + dt.new_widget("label"){label = _("reset to original time")}, + dt.new_widget("separator"){}, + dt.new_widget("button"){ + label = _("reset"), + clicked_callback = function() + reset_time(dt.gui.action_images) + end + } + }, +} + +img_time.mode = dt.new_widget("combobox"){ + label = _("mode"), + tooltip = _("select mode"), + selected = 1, + changed_callback = function(this) + img_time.stack.active = this.selected + end, + _("adjust time"), + _("set time"), + _("synchronize time"), + _("reset time") +} + +img_time.widget = dt.new_widget("box"){ + orientation = "vertical", + reset_callback = function(this) + reset_widgets() + end, + img_time.mode, + img_time.stack, +} + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not img_time.event_registered then + dt.register_event( + "image_time", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + img_time.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 diff --git a/contrib/jpg_group_leader.lua b/contrib/jpg_group_leader.lua new file mode 100644 index 00000000..733071ed --- /dev/null +++ b/contrib/jpg_group_leader.lua @@ -0,0 +1,200 @@ +--[[ + + jpg_group_leader.lua - Make jpg image group leader + + Copyright (C) 2024 Bill Ferguson . + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +]] +--[[ + jpg_group_leader - Make jpg image group leader + + After a film roll is imported, check for RAW-JPG image groups + and make the JPG image the group leader. This is on by default + but can be disabled in preferences. + + Shortcuts are included to filter existing collections or + selections of images and make the jpg the group leader. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + None + + USAGE + Start script from script_manager + Assign keys to the shortcuts + + BUGS, COMMENTS, SUGGESTIONS + Bill Ferguson + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "jpg_group_leader" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("7.0.0", MODULE) + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- I 1 8 N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- S C R I P T M A N A G E R I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local script_data = {} + +script_data.metadata = { + name = _("JPG group leader"), + purpose = _("make jpg image group leader"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/jpg_group_leader" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- P R E F E R E N C E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.preferences.register(MODULE, "on_import", "bool", _("make jpg group leader on import"), _("automatically make the jpg file the group leader when raw + jpg are imported"), true) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local jgloi = {} +jgloi.images = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function toggle_global_toolbox_grouping() + dt.gui.libs.global_toolbox.grouping = false + dt.gui.libs.global_toolbox.grouping = true +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N P R O G R A M +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function make_jpg_group_leader(images) + -- if the image is part of a group, make it the leader + for _, image in ipairs(images) do + if #image:get_group_members() > 1 then + image:make_group_leader() + end + end + if dt.gui.libs.global_toolbox.grouping then + -- toggle the grouping to make the new leader show + toggle_global_toolbox_grouping() + end +end + +local function make_existing_jpg_group_leader(images) + for _, image in ipairs(images) do + if string.lower(df.get_filetype(image.filename)) == "jpg" then + if #image:get_group_members() > 1 then + image:make_group_leader() + end + end + end + if dt.gui.libs.global_toolbox.grouping then + -- toggle the grouping to make the new leader show + toggle_global_toolbox_grouping() + end +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + if dt.preferences.read(MODULE, "on_import", "bool") then + dt.destroy_event(MODULE, "post-import-film") + dt.destroy_event(MODULE, "post-import-image") + end + dt.destroy_event(MODULE .. "_collect", "shortcut") + dt.destroy_event(MODULE .. "_select", "shortcut") +end + +script_data.destroy = destroy + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +if dt.preferences.read(MODULE, "on_import", "bool") then + dt.register_event(MODULE, "post-import-film", + function(event, film_roll) + -- ignore the film roll, it contains all the images, not just the imported + local images = jgloi.images + if #images > 0 then + jgloi.images = {} + make_jpg_group_leader(images) + end + end + ) + + dt.register_event(MODULE, "post-import-image", + function(event, image) + if string.lower(df.get_filetype(image.filename)) == "jpg" then + table.insert(jgloi.images, image) + end + end + ) +end + +dt.register_event(MODULE .. "_collect", "shortcut", + function(event, shortcut) + -- ignore the film roll, it contains all the images, not just the imported + local images = dt.collection + make_existing_jpg_group_leader(images) + end, + _("make jpg group leader for collection") +) + +dt.register_event(MODULE .. "_select", "shortcut", + function(event, shortcut) + local images = dt.gui.selection() + make_existing_jpg_group_leader(images) + end, + _("make jpg group leader for selection") +) + +return script_data \ No newline at end of file diff --git a/contrib/kml_export.lua b/contrib/kml_export.lua index ab1f8c06..561724fa 100644 --- a/contrib/kml_export.lua +++ b/contrib/kml_export.lua @@ -1,407 +1,394 @@ --[[ - This file is part of darktable, - Copyright 2016 by Tobias Jakobs. - Copyright 2016 by Erik Augustin. - - 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 . + This file is part of darktable, + Copyright 2018 by Tobias Jakobs. + Copyright 2018 by Erik Augustin. + + 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 . ]] --[[ darktable KML export script ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT -* mkdir -* zip (only if you create KMZ files) -* convert (ImageMagick) -* xdg-open -* xdg-user-dir +* zip (at the moment Linux only and only if you create KMZ files) +* magick (ImageMagick) +* xdg-user-dir (Linux) WARNING This script is only tested with Linux USAGE -* require script "official/yield" from your main Lua file in the first line * require this script from your main Lua file * when choosing file format, pick JPEG or PNG as Google Earth doesn't support other formats ]] local dt = require "darktable" +local du = require "lib/dtutils" local df = require "lib/dtutils.file" -require "official/yield" -local gettext = dt.gettext +local ds = require "lib/dtutils.string" +local dsys = require "lib/dtutils.system" -dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0}) +local gettext = dt.gettext.gettext --- 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 PS = dt.configuration.running_os == "windows" and "\\" or "/" + +du.check_min_api_version("7.0.0", "kml_export") local function _(msgid) - return gettext.dgettext("kml_export", msgid) + return gettext(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 - local keys = {} - for _key in pairs(_table) do keys[#keys + 1] = _key end +-- return data structure for script_manager - -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys - if order then - table.sort(keys, function(a,b) return order(_table, a, b) end) - else - table.sort(keys) - end +local script_data = {} - -- return the iterator function - local i = 0 - return function() - i = i + 1 - if keys[i] then - return keys[i], _table[keys[i]] - end - end -end +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 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 --- Strip accents from a string --- Copied from https://forums.coronalabs.com/topic/43048-remove-special-characters-from-string/ -function string.stripAccents( str ) - local tableAccents = {} - tableAccents["à"] = "a" - tableAccents["á"] = "a" - tableAccents["â"] = "a" - tableAccents["ã"] = "a" - tableAccents["ä"] = "a" - tableAccents["ç"] = "c" - tableAccents["è"] = "e" - tableAccents["é"] = "e" - tableAccents["ê"] = "e" - tableAccents["ë"] = "e" - tableAccents["ì"] = "i" - tableAccents["í"] = "i" - tableAccents["î"] = "i" - tableAccents["ï"] = "i" - tableAccents["ñ"] = "n" - tableAccents["ò"] = "o" - tableAccents["ó"] = "o" - tableAccents["ô"] = "o" - tableAccents["õ"] = "o" - tableAccents["ö"] = "o" - tableAccents["ù"] = "u" - tableAccents["ú"] = "u" - tableAccents["û"] = "u" - tableAccents["ü"] = "u" - tableAccents["ý"] = "y" - tableAccents["ÿ"] = "y" - tableAccents["À"] = "A" - tableAccents["Á"] = "A" - tableAccents["Â"] = "A" - tableAccents["Ã"] = "A" - tableAccents["Ä"] = "A" - tableAccents["Ç"] = "C" - tableAccents["È"] = "E" - tableAccents["É"] = "E" - tableAccents["Ê"] = "E" - tableAccents["Ë"] = "E" - tableAccents["Ì"] = "I" - tableAccents["Í"] = "I" - tableAccents["Î"] = "I" - tableAccents["Ï"] = "I" - tableAccents["Ñ"] = "N" - tableAccents["Ò"] = "O" - tableAccents["Ó"] = "O" - tableAccents["Ô"] = "O" - tableAccents["Õ"] = "O" - tableAccents["Ö"] = "O" - tableAccents["Ù"] = "U" - tableAccents["Ú"] = "U" - tableAccents["Û"] = "U" - tableAccents["Ü"] = "U" - tableAccents["Ý"] = "Y" - - local normalizedString = "" - - for strChar in string.gmatch(str, "([%z\1-\127\194-\244][\128-\191]*)") do - if tableAccents[strChar] ~= nil then - normalizedString = normalizedString..tableAccents[strChar] - else - normalizedString = normalizedString..strChar +-- Add duplicate index to filename +-- image.filename does not have index, exported_image has index +function addDuplicateIndex( index, filename ) + if index > 0 then + filename = filename.."_" + if index < 10 then + filename = filename.."0" end + filename = filename..index end - return normalizedString - + return filename end --- Escape XML characters --- Keep & first, otherwise it will double escape other characters --- https://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents -function string.escapeXmlCharacters( str ) - - str = string.gsub(str,"&", "&") - str = string.gsub(str,"\"", """) - str = string.gsub(str,"'", "'") - str = string.gsub(str,"<", "<") - str = string.gsub(str,">", ">") +local function create_kml_file(storage, image_table, extra_data) - return str -end + local magickPath + if dt.configuration.running_os == "linux" then + magickPath = 'convert' + else + magickPath = dt.preferences.read("kml_export","magickPath","string") + end -local function create_kml_file(storage, image_table, extra_data) - if not df.check_if_bin_exists("mkdir") then - dt.print_error(_("mkdir not found")) - return - end - if not df.check_if_bin_exists("convert") then - 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")) - return - end + if not df.check_if_bin_exists(magickPath) then + 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")) - return + dt.print_error("xdg-user-dir not found") + return end - - dt.print_error("Will try to export KML file now") - - local imageFoldername - if ( dt.preferences.read("kml_export","CreateKMZ","bool") == true ) then - if not df.check_if_bin_exists("zip") then - dt.print_error(_("zip not found")) - return - end - - exportDirectory = dt.configuration.tmp_dir - imageFoldername = "" - else - exportDirectory = dt.preferences.read("kml_export","ExportDirectory","string") - - -- Creates dir if not exsists - imageFoldername = "files/" - local mkdirCommand = "mkdir -p "..exportDirectory.."/"..imageFoldername - dt.control.execute(mkdirCommand) + end + dt.print_log("Will try to export KML file now") + + local imageFoldername + 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") + return end + exportDirectory = dt.configuration.tmp_dir + imageFoldername = "" + else + exportDirectory = dt.preferences.read("kml_export","ExportDirectory","string") + -- Creates dir if not exsists + imageFoldername = "files"..PS + df.mkdir(df.sanitize_filename(exportDirectory..PS..imageFoldername)) + end + -- Create the thumbnails + for image,exported_image in pairs(image_table) 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 + local path, filename, filetype = string.match(exported_image, "(.-)([^\\/]-%.?([^%.\\/]*))$") + filename = string.upper(string.gsub(filename,"%.%w*", "")) - -- Create the thumbnails - for image,exported_image in pairs(image_table) 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 - local path, filename, filetype = string.match(exported_image, "(.-)([^\\/]-%.?([^%.\\/]*))$") - filename = string.upper(string.gsub(filename,"%.%w*", "")) - - -- convert -size 92x92 filename.jpg -resize 92x92 +profile "*" thumbnail.jpg - -- In this example, '-size 120x120' gives a hint to the JPEG decoder that the image is going to be downscaled to - -- 120x120, allowing it to run faster by avoiding returning full-resolution images to GraphicsMagick for the - -- subsequent resizing operation. The '-resize 120x120' specifies the desired dimensions of the output image. It - -- will be scaled so its largest dimension is 120 pixels. The '+profile "*"' removes any ICM, EXIF, IPTC, or other - -- profiles that might be present in the input and aren't needed in the thumbnail. - - local convertToThumbCommand = "convert -size 96x96 "..exported_image.." -resize 92x92 -mattecolor \"#FFFFFF\" -frame 2x2 +profile \"*\" "..exportDirectory.."/"..imageFoldername.."thumb_"..filename..".jpg" - dt.control.execute(convertToThumbCommand) - else - -- Remove exported image if it has no GPS data - os.remove(exported_image) - end + -- magick -size 92x92 filename.jpg -resize 92x92 +profile "*" thumbnail.jpg + -- In this example, '-size 120x120' gives a hint to the JPEG decoder that the image is going to be downscaled to + -- 120x120, allowing it to run faster by avoiding returning full-resolution images to GraphicsMagick for the + -- subsequent resizing operation. The '-resize 120x120' specifies the desired dimensions of the output image. It + -- will be scaled so its largest dimension is 120 pixels. The '+profile "*"' removes any ICM, EXIF, IPTC, or other + -- profiles that might be present in the input and aren't needed in the thumbnail. - local pattern = "[/]?([^/]+)$" - filmName = string.match(image.film.path, pattern) + local convertToThumbCommand = ds.sanitize(magickPath) .. " -size 96x96 " .. exported_image .. " -resize 92x92 -mattecolor \"#FFFFFF\" -frame 2x2 +profile \"*\" " .. exportDirectory .. PS .. imageFoldername .. "thumb_" .. filename .. ".jpg" - -- Strip accents from the filename, because GoogleEarth can't open them - -- https://github.com/darktable-org/lua-scripts/issues/54 - filmName = string.stripAccents(filmName) + if (exported_image ~= exportDirectory..PS..imageFoldername..filename.."."..filetype) then + df.file_copy(exported_image, exportDirectory..PS..imageFoldername..filename.."."..filetype) + end + dsys.external_command(convertToThumbCommand) + + else + -- Remove exported image if it has no GPS data + os.remove(exported_image) end - exportKMLFilename = filmName..".kml" - exportKMZFilename = filmName..".kmz" + local pattern = "[/]?([^/]+)$" + filmName = string.match(image.film.path, pattern) + + -- Strip accents from the filename, because GoogleEarth can't open them + -- https://github.com/darktable-org/lua-scripts/issues/54 + filmName = ds.strip_accents(filmName) + + -- Remove chars we don't like to have in filenames + filmName = string.gsub(filmName, [[\]], "") + filmName = string.gsub(filmName, [[/]], "") + filmName = string.gsub(filmName, [[:]], "") + filmName = string.gsub(filmName, [["]], "") + filmName = string.gsub(filmName, "<", "") + filmName = string.gsub(filmName, ">", "") + filmName = string.gsub(filmName, "|", "") + filmName = string.gsub(filmName, "*", "") + filmName = string.gsub(filmName, "?", "") + filmName = string.gsub(filmName,'[.]', "") -- At least Windwows has problems with the "." and the start command + end - -- Create the KML file - local kml_file = "\n" - kml_file = kml_file.."\n" - kml_file = kml_file.."\n" + exportKMLFilename = filmName..".kml" + exportKMZFilename = filmName..".kmz" - --image_table = dt.gui.selection(); - kml_file = kml_file..""..filmName.."\n" - kml_file = kml_file.." Exported from darktable\n" + -- Create the KML file + local kml_file = "\n" + kml_file = kml_file.."\n" + kml_file = kml_file.."\n" - for image,exported_image in pairs(image_table) do + --image_table = dt.gui.selection(); + kml_file = kml_file..""..filmName.."\n" + kml_file = kml_file.." Exported from darktable\n" + + for image,exported_image in pairs(image_table) do -- Extract filename, e.g DSC9784.ARW -> DSC9784 filename = string.upper(string.gsub(image.filename,"%.%w*", "")) + -- Handle duplicates + filename = addDuplicateIndex( image.duplicate_index, filename ) -- Extract extension from exported image (user can choose JPG or PNG), e.g DSC9784.JPG -> .JPG extension = string.match(exported_image,"%.%w*$") - 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 - kml_file = kml_file.." \n" - - local image_title, image_description - if (image.title and image.title ~= "") then - image_title = string.escapeXmlCharacters(image.title) - else - image_title = image.filename - end - -- Characters should not be escaped in CDATA, but we are using HTML fragment, so we must escape them - image_description = string.escapeXmlCharacters(image.description) - - kml_file = kml_file.." "..image_title.."\n" - kml_file = kml_file.." "..image_description.."\n" - kml_file = kml_file.." \n" - - kml_file = kml_file.." \n" - kml_file = kml_file.." 1\n" - kml_file = kml_file.." "..string.gsub(tostring(image.longitude),",", ".")..","..string.gsub(tostring(image.latitude),",", ".")..",0\n" - kml_file = kml_file.." \n" - kml_file = kml_file.." "..string.gsub(image.exif_datetime_taken," ", "T").."Z".."\n" - kml_file = kml_file.." \n" - kml_file = kml_file.." \n" - - kml_file = kml_file.." \n" - end - end - --- Connects all images with an path - if ( dt.preferences.read("kml_export","CreatePath","bool") == true ) then + 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 kml_file = kml_file.." \n" - kml_file = kml_file.." Path\n" -- ToDo: I think a better name would be nice - --kml_file = kml_file.." \n" - - kml_file = kml_file.." \n" - - kml_file = kml_file.." \n" - kml_file = kml_file.." \n" - - for image,exported_image in spairs(image_table, function(t,a,b) return t[b] < t[a] end) 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 - local altitude = 0; - if (image.elevation) then - altitude = image.elevation; - end - kml_file = kml_file.." "..string.gsub(tostring(image.longitude),",", ".")..","..string.gsub(tostring(image.latitude),",", ".")..",altitude\n" - end + + local image_title, image_description + if (image.title and image.title ~= "") then + image_title = ds.escape_xml_characters(image.title) + else + image_title = filename..extension end - kml_file = kml_file.." \n" - kml_file = kml_file.." \n" + -- Characters should not be escaped in CDATA, but we are using HTML fragment, so we must escape them + image_description = ds.escape_xml_characters(image.description) + + kml_file = kml_file.." "..image_title.."\n" + kml_file = kml_file.." "..image_description.."\n" + kml_file = kml_file.." \n" + + kml_file = kml_file.." \n" + kml_file = kml_file.." 1\n" + kml_file = kml_file.." "..string.gsub(tostring(image.longitude),",", ".")..","..string.gsub(tostring(image.latitude),",", ".")..",0\n" + kml_file = kml_file.." \n" + kml_file = kml_file.." "..string.gsub(image.exif_datetime_taken," ", "T").."Z".."\n" + kml_file = kml_file.." \n" + kml_file = kml_file.." \n" kml_file = kml_file.." \n" end + end + + -- Connects all images with an path + if ( dt.preferences.read("kml_export","CreatePath","bool") == true ) then + kml_file = kml_file.." \n" + kml_file = kml_file.." Path\n" -- ToDo: I think a better name would be nice + --kml_file = kml_file.." \n" + + kml_file = kml_file.." \n" + + kml_file = kml_file.." \n" + kml_file = kml_file.." \n" + + for image,exported_image in du.spairs(image_table, function(t,a,b) return b.exif_datetime_taken > a.exif_datetime_taken end) 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 + local altitude = 0; + if (image.elevation) then + altitude = image.elevation; + end + + kml_file = kml_file.." "..string.gsub(tostring(image.longitude),",", ".")..","..string.gsub(tostring(image.latitude),",", ".")..",altitude\n" + end + end + + kml_file = kml_file.." \n" + kml_file = kml_file.." \n" - kml_file = kml_file.."\n" - kml_file = kml_file.."" + kml_file = kml_file.." \n" + end - local file = io.open(exportDirectory.."/"..exportKMLFilename, "w") - file:write(kml_file) - file:close() + kml_file = kml_file.."\n" + kml_file = kml_file.."" - dt.print("KML file created in "..exportDirectory) + local file = io.open(exportDirectory..PS..exportKMLFilename, "w") --- Compress the files to create a KMZ file - if ( dt.preferences.read("kml_export","CreateKMZ","bool") == true ) then - exportDirectory = dt.preferences.read("kml_export","ExportDirectory","string") + file:write(kml_file) + file:close() - local createKMZCommand = "zip --test --move --junk-paths " - createKMZCommand = createKMZCommand .."\""..exportDirectory.."/"..exportKMZFilename.."\" " -- KMZ filename - createKMZCommand = createKMZCommand .."\""..dt.configuration.tmp_dir.."/"..exportKMLFilename.."\" " -- KML file + dt.print("KML file created in "..exportDirectory) - for image,exported_image in pairs(image_table) 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 - local filename = string.upper(string.gsub(image.filename,"%.%w*", "")) + -- Compress the files to create a KMZ file + if ( dt.preferences.read("kml_export","CreateKMZ","bool") == true + and dt.configuration.running_os == "linux") then + exportDirectory = dt.preferences.read("kml_export","ExportDirectory","string") - createKMZCommand = createKMZCommand .."\""..dt.configuration.tmp_dir.."/"..imageFoldername.."thumb_"..filename..".jpg\" " -- thumbnails - createKMZCommand = createKMZCommand .."\""..exported_image.."\" " -- images - end - end + local createKMZCommand = "zip --test --move --junk-paths " + createKMZCommand = createKMZCommand .."\""..exportDirectory..PS..exportKMZFilename.."\" " -- KMZ filename + createKMZCommand = createKMZCommand .."\""..dt.configuration.tmp_dir..PS..exportKMLFilename.."\" " -- KML file - dt.control.execute(createKMZCommand) + for image,exported_image in pairs(image_table) 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 + local filename = string.upper(string.gsub(image.filename,"%.%w*", "")) + -- Handle duplicates + filename = addDuplicateIndex( image.duplicate_index, filename ) + + createKMZCommand = createKMZCommand .."\""..dt.configuration.tmp_dir..PS..imageFoldername.."thumb_"..filename..".jpg\" " -- thumbnails + createKMZCommand = createKMZCommand .."\""..exported_image.."\" " -- images + end end --- Open the file with the standard programm - if ( dt.preferences.read("kml_export","OpenKmlFile","bool") == true ) then - local kmlFileOpenCommand + dt.control.execute(createKMZCommand) + end + + -- Open the file with the standard programm + if ( dt.preferences.read("kml_export","OpenKmlFile","bool") == true ) then + local path - if ( dt.preferences.read("kml_export","CreateKMZ","bool") == true ) then - kmlFileOpenCommand = "xdg-open "..exportDirectory.."/\""..exportKMZFilename.."\"" - else - kmlFileOpenCommand = "xdg-open "..exportDirectory.."/\""..exportKMLFilename.."\"" - end - dt.control.execute(kmlFileOpenCommand) + if ( dt.preferences.read("kml_export","CreateKMZ","bool") == true + and dt.configuration.running_os == "linux") then + path = exportDirectory..PS..exportKMZFilename + else + path = exportDirectory..PS..exportKMLFilename end + dsys.launch_default_app(df.sanitize_filename(path)) + end + +end +local function destroy() + dt.destroy_storage("kml_export") end -- Preferences -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"), - false ) - -local handle = io.popen("xdg-user-dir DESKTOP") -local result = handle:read() -if (result == nil) then - result = "" +if dt.configuration.running_os == "windows" then + dt.preferences.register("kml_export", + "OpenKmlFile", + "bool", + _("KML export: Open KML file after export"), + _("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"), + false ) end -handle:close() -dt.preferences.register("kml_export", - "ExportDirectory", - "directory", - _("KML export: Export directory"), - _("A directory that will be used to export the KML/KMZ files"), - result ) + +local defaultDir = '' +if dt.configuration.running_os == "windows" then + defaultDir = os.getenv("USERPROFILE") +elseif dt.configuration.running_os == "macos" then + defaultDir = os.getenv("HOME") +else + local handle = io.popen("xdg-user-dir DESKTOP") + defaultDir = handle:read() + handle:close() +end + dt.preferences.register("kml_export", - "CreatePath", - "bool", - _("KML export: Connect images with path"), - _("connect all images with a path"), - false ) + "ExportDirectory", + "directory", + _("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 + "magick") -- default +end + dt.preferences.register("kml_export", - "CreateKMZ", - "bool", - _("KML export: Create KMZ file"), - _("Compress all imeges to one KMZ file"), - true ) + "CreatePath", + "bool", + _("KML export: connect images with path"), + _("connect all images with a path"), + false ) + +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"), + true ) +end -- Register -dt.register_storage("kml_export", _("KML/KMZ Export"), nil, create_kml_file) +if dt.configuration.running_os == "windows" then + dt.register_storage("kml_export", _("KML export"), nil, create_kml_file) +else + dt.register_storage("kml_export", _("KML/KMZ export"), nil, create_kml_file) +end + +script_data.destroy = destroy + +return script_data + +-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua +-- kate: hl Lua; diff --git a/contrib/passport_guide.lua b/contrib/passport_guide.lua index 48ae64f6..34e9fc79 100644 --- a/contrib/passport_guide.lua +++ b/contrib/passport_guide.lua @@ -20,7 +20,7 @@ --[[ PASSPORT CROPPING GUIDE guides for cropping passport photos based on documents from the Finnish police -(https://www.poliisi.fi/instancedata/prime_product_julkaisu/intermin/embeds/poliisiwwwstructure/38462_Passikuvaohje_EN.pdf) describing passport photo dimensions of 47x36 mm and 500x653 px for digital biometric data stored in passports. They use ISO 19794-5 standard based on ICAO 9303 regulations which should also be compliant for all of Europe. +(https://poliisi.fi/documents/25235045/31329600/Passport-photograph-instructions-by-the-police-2020-EN-fixed.pdf/1eec2f4c-aed7-68e0-c112-0a8f25e0328d/Passport-photograph-instructions-by-the-police-2020-EN-fixed.pdf) describing passport photo dimensions of 47x36 mm and 500x653 px for digital biometric data stored in passports. They use ISO 19794-5 standard based on ICAO 9303 regulations which should also be compliant for all of Europe. INSTALLATION * copy this file in $CONFIGDIR/lua/ where CONFIGDIR is your darktable configuration directory @@ -36,17 +36,29 @@ USAGE ]] local dt = require "darktable" -local gettext = dt.gettext +local du = require "lib/dtutils" +local gettext = dt.gettext.gettext -dt.configuration.check_version(...,{2,0,0},{3,0,0},{4,0,0},{5,0,0}) - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("passport_guide",dt.configuration.config_dir.."/lua/locale/") +du.check_min_api_version("2.0.0", "passport_guide") local function _(msgid) - return gettext.dgettext("passport_guide", msgid) + return gettext(msgid) end +local script_data = {} + +script_data.metadata = { + name = _("passport guide"), + purpose = _("guides for cropping passport photos"), + author = "Kåre Hampf", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/passport_guide" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + dt.guides.register_guide("passport", -- draw function(cairo, x, y, width, height, zoom_scale) @@ -90,5 +102,13 @@ function() end ) +local function destroy() + -- nothing to destroy +end + +script_data.destroy = destroy + +return script_data + -- kate: tab-indents: off; indent-width 2; replace-tabs on; remove-trailing-space on; hl Lua; -- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua diff --git a/contrib/passport_guide_germany.lua b/contrib/passport_guide_germany.lua new file mode 100644 index 00000000..a1b9f618 --- /dev/null +++ b/contrib/passport_guide_germany.lua @@ -0,0 +1,125 @@ +--[[ + German passport photo cropping guide for darktable. + Derived from the passport cropping guide by Kåre Hampf. + + copyright (c) 2017 Kåre Hampf + copyright (c) 2024 Christian Sültrop + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +]] + +--[[ +PASSPORT CROPPING GUIDE +Guides for cropping passport and ID card ("Personalausweis") photos based on the "Passbild-Schablone" +from the German Federal Ministry of the Interior and Community. +(https://www.bmi.bund.de/SharedDocs/downloads/DE/veroeffentlichungen/themen/moderne-verwaltung/ausweise/passbild-schablone-erwachsene.pdf?__blob=publicationFile&v=3) + +INSTALLATION +* copy this file in $CONFIGDIR/lua/ where CONFIGDIR is your darktable configuration directory +* add the following line in the file $CONFIGDIR/luarc + require "passport_guide_germany" +* (optional) add the line: + "plugins/darkroom/clipping/extra_aspect_ratios/passport 35x45mm=45:35" + to $CONFIGDIR/darktablerc + +USAGE +* when using the cropping tool, select "Passport Photo Germany" as guide and if you added the line in yout rc + select "passport 35x45mm" as aspect +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local gettext = dt.gettext.gettext + +du.check_min_api_version("2.0.0", "passport_guide_germany") + +-- Tell gettext where to find the .mo file translating messages for a particular domain +local function _(msgid) + return gettext(msgid) +end + +local script_data = {} + +script_data.metadata = { + name = _("passport guide Germany"), + purpose = _("guides for cropping passport and ID card (\"Personalausweis\") photos based on the \"Passbild-Schablone\" from the German Federal Ministry of the Interior and Community"), + author = "menschmachine", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/passport_guide_germany" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +dt.guides.register_guide("Passport Photo Germany", +-- draw +function(cairo, x, y, width, height, zoom_scale) + local _width, _height + + -- get the max 35x45 rectangle + local aspect_ratio = 45 / 35 + if width * aspect_ratio > height then + _width = height / aspect_ratio + _height = height + else + _width = width + _height = width * aspect_ratio + end + + cairo:save() + + cairo:translate(x + (width - _width) / 2, y + (height - _height) / 2) + cairo:scale(_width / 35, _height / 45) + + -- the outer rectangle + cairo:rectangle( 0, 0, 35, 45) + + -- Nose position: The nose tip must be between these lines + cairo:draw_line(15.5, 45, 15.5, 13) + cairo:draw_line(35-15.5, 45, 35-15.5, 13) + + -- Face height + -- optimum face height: The upper end of the head should be between these lines + cairo:draw_line(0, 4, 35, 4) + cairo:draw_line(0, 8, 35, 8) + + -- tolerated face height: The upper end of the head must not be below this line + cairo:draw_line(6, 13, 30, 13) + + -- Eye area: The eyes must be between these lines + cairo:draw_line(0, 13, 35, 13) + cairo:draw_line(0, 23, 35, 23) + + -- Cheek line: The cheek must lie on this line + cairo:draw_line(9, 45-5, 27, 45-5) + + cairo:restore() +end, +-- gui +function() + return dt.new_widget("label"){label = _("Passport Photo Germany"), halign = "start"} +end +) + +local function destroy() + -- noting to destroy +end + +script_data.destroy = destroy + +return script_data + +-- kate: tab-indents: off; indent-width 2; replace-tabs on; remove-trailing-space on; hl Lua; +-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua diff --git a/contrib/pdf_slideshow.lua b/contrib/pdf_slideshow.lua index d5a71d40..b93ae978 100644 --- a/contrib/pdf_slideshow.lua +++ b/contrib/pdf_slideshow.lua @@ -39,24 +39,37 @@ format (all fields can be the empty string): ]] local dt = require "darktable" +local du = require "lib/dtutils" local df = require "lib/dtutils.file" -require "official/yield" -local gettext = dt.gettext - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("pdf_slideshow",dt.configuration.config_dir.."/lua/locale/") +local gettext = dt.gettext.gettext local function _(msgid) - return gettext.dgettext("pdf_slideshow", msgid) + return gettext(msgid) end if not df.check_if_bin_exists("pdflatex") then - dt.print_error(_("pdflatex not found")) + dt.print_error("pdflatex not found") return end -dt.configuration.check_version(...,{4,0,0},{5,0,0}) +du.check_min_api_version("7.0.0", "pdf_slideshow") + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("PDF slideshow"), + purpose = _("generates a PDF slideshow (via Latex) containing all selected images one per slide"), + author = "Pascal Obry", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/pdf_slideshow" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them dt.preferences.register ("pdf_slideshow","open with","string", @@ -164,6 +177,10 @@ local function slide(latexfile,i,image,file) my_write(latexfile,"\\end{figure}\n") end +local function destroy() + dt.destroy_storage("pdf_slideshow") +end + dt.register_storage("pdf_slideshow",_("pdf slideshow"), nil, function(storage,image_table) @@ -218,7 +235,7 @@ dt.register_storage("pdf_slideshow",_("pdf slideshow"), local result = dt.control.execute(command) if result ~= 0 then dt.print(_("problem running pdflatex")) -- this one is probably usefull to the user - error(_("problem running ")..command) + error("problem running "..command) end -- open the PDF @@ -228,7 +245,7 @@ dt.register_storage("pdf_slideshow",_("pdf slideshow"), local result = dt.control.execute(command) if result ~= 0 then dt.print(_("problem running pdf viewer")) -- this one is probably usefull to the user - error(_("problem running ")..command) + error("problem running "..command) end -- finally do some clean-up @@ -243,5 +260,8 @@ dt.register_storage("pdf_slideshow",_("pdf slideshow"), nil, widget) +script_data.destroy = destroy + +return script_data -- -- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua diff --git a/contrib/photils.lua b/contrib/photils.lua new file mode 100644 index 00000000..7bb39c6f --- /dev/null +++ b/contrib/photils.lua @@ -0,0 +1,511 @@ +--[[ photils Auto Tagging plugin + copyright (c) 2020 Tobias Scheck + + 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 . +--]] + +--[[ + A darktable plugin that tries to predict keywords based on the selected image. + This plugin uses photils-cli to handle this task. Photils-cli is an application + that passes the image through a neural network, classifies it, and extracts the + suggested tags. Everything happens offline without the need that your data are + sent over the internet. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + * photils-cli - https://github.com/scheckmedia/photils-cli at the moment only + available for Linux and MacOS + + USAGE + * require this script from your main lua file + To do this add this line to the file .config/darktable/luarc: + require "contrib/photils" + * Select an image + * Press "get tags" + * Select the tags you want from a list of suggestions + * Press "Attach .. Tags" to add the selected tags to your image +--]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local dtsys = require "lib/dtutils.system" + +local MODULE_NAME = "photils" +du.check_min_api_version("7.0.0", MODULE_NAME) + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("photils"), + purpose = _("suggest tags based on image classification"), + author = "Tobias Scheck", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/photils" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + + +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +local exporter = dt.new_format("jpeg") +exporter.quality = 80 +exporter.max_height = 224 +exporter.max_width = 224 + +-- helper functions + +local function num_keys(tbl) + local num = 0 + for _ in pairs(tbl) do num = num + 1 end + return num +end + +local function has_key(tbl, value) + for k, _ in pairs(tbl) do + if k == value then + return true + end + end + + return false +end + +local photils_installed = df.check_if_bin_exists("photils-cli") + +--[[ + local state object + + maybe per_page is a preference variable but I think 10 + is a good value for the ui +]] +local PHOTILS = { + tags = {}, + confidences = {}, + page = 1, + per_page = 10, + selected_tags = {}, + in_pagination = false, + tagged_image = "", + module_installed = false, + event_registered = false, + plugin_display_views = { + [dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}, + [dt.gui.views.darkroom] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 100} + }, +} + +local GUI = { + container = dt.new_widget("box") { + orientation = "vertical", + sensitive = true, + dt.new_widget("button") { + label = _("get tags"), + sensitive = photils_installed, + clicked_callback = function() PHOTILS.on_tags_clicked() end + }, + reset_callback = function() PHOTILS.on_reset(true) end + }, + stack = dt.new_widget("stack"), + prev_button = dt.new_widget("button") { + label = "<", + sensitive = false, + clicked_callback = function() + PHOTILS.page = PHOTILS.page - 1 + PHOTILS.paginate() + end + }, + next_button = dt.new_widget("button") { + label = ">", + sensitive = false, + clicked_callback = function() + PHOTILS.page = PHOTILS.page + 1 + PHOTILS.paginate() + end + }, + tag_box = dt.new_widget("box") {orientation = "vertical"}, + tag_view = dt.new_widget("box") {orientation = "vertical"}, + page_label = dt.new_widget("label") {label = ""}, + error_view = dt.new_widget("box") {orientation = "vertical"}, + warning_label = dt.new_widget("label") { + label = "" + }, + restart_required_label = dt.new_widget("label") { + label = _("requires a restart to be applied") + }, + attach_button = dt.new_widget("button") { + label = "", + sensitive = false, + clicked_callback = function(self) PHOTILS.attach_tags() end + }, + confidence_slider = dt.new_widget("slider") { + step = 1, + digits = 0, + value = 90, + hard_max = 100, + hard_min = 0, + soft_max = 100, + soft_min = 0, + label = _("min confidence value") + }, + warning = dt.new_widget("label") +} + +function PHOTILS.image_changed() + local current_image = tostring(dt.gui.selection()[1]) + if current_image ~= PHOTILS.tagged_image then + if PHOTILS.tagged_image ~= "" then + PHOTILS.tagged_image_has_changed() + end + + PHOTILS.tagged_image = tostring(current_image) + end +end + +function PHOTILS.tagged_image_has_changed() + GUI.warning.label = _("the suggested tags were not generated\n for the currently selected image!") +end + +function PHOTILS.paginate() + PHOTILS.in_pagination = true + local num_pages = math.ceil(#PHOTILS.tags / PHOTILS.per_page) + GUI.page_label.label = string.format(_(" page %s of %s "), PHOTILS.page, + num_pages) + + if PHOTILS.page <= 1 then + PHOTILS.page = 1 + GUI.prev_button.sensitive = false + else + GUI.prev_button.sensitive = true + end + + if PHOTILS.page > num_pages - 1 then + PHOTILS.page = num_pages + GUI.next_button.sensitive = false + else + GUI.next_button.sensitive = true + end + + --[[ + calculates the start positon in the tag array based on the current page + and takes N tags from that array to show these in darktable + e.g. page 1 goes from 1 to 10, page 2 from 11 to 20 a.s.o. + the paginaton approach is related to a problem with the dynamic addition + of mutliple widgets https://github.com/darktable-org/darktable/issues/4934#event-3318100463 + ]]-- + local offset = ((PHOTILS.page - 1) * PHOTILS.per_page) + 1 + local tag_index = 1 + for i = offset, offset + PHOTILS.per_page - 1, 1 do + local tag = PHOTILS.tags[i] + local conf = PHOTILS.confidences[i] + + GUI.tag_box[tag_index].value = has_key(PHOTILS.selected_tags, tag) + + if tag then + if dt.preferences.read(MODULE_NAME, "show_confidence", "bool") then + tag = tag .. string.format(" (%.3f)", conf) + end + + GUI.tag_box[tag_index].label = tag + GUI.tag_box[tag_index].sensitive = true + else + GUI.tag_box[tag_index].label = "" + GUI.tag_box[tag_index].sensitive = false + end + tag_index = tag_index + 1 + end + + PHOTILS.in_pagination = false +end + +function PHOTILS.attach_tags() + local num_selected = #dt.gui.selection() + local job = dt.gui.create_job(_("apply tag to image"), true) + + for i = 1, num_selected, 1 do + local image = dt.gui.selection()[i] + for tag, _ in pairs(PHOTILS.selected_tags) do + local dt_tag = dt.tags.create(tag) + dt.tags.attach(dt_tag, image) + end + + job.percent = i / num_selected + end + + dt.print(_("tags successfully attached to image")) + job.valid = false +end + +function PHOTILS.get_tags(image, with_export) + + local tmp_file = df.create_tmp_file() + local in_arg = df.sanitize_filename(tostring(image)) + 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() + exporter:write_image(image, export_file) + in_arg = df.sanitize_filename(tostring(export_file)) + end + + local command = executable .. " -c " .. " -i " .. in_arg .. " -o " .. out_arg + + local ret = dtsys.external_command(command) + if ret > 0 then + dt.print_error(string.format("command %s returned error code %d", command, ret)) + os.remove(tmp_file) + + -- try to export the image and run tagging + if not with_export then + return PHOTILS.get_tags(image, true) + end + + return false + end + + for i = #PHOTILS.tags, 1, -1 do + PHOTILS.tags[i] = nil + PHOTILS.confidences[i] = nil + end + + for tag in io.lines(tmp_file) do + local splitted = du.split(tag, ":") + if 100 * tonumber(splitted[2]) >= GUI.confidence_slider.value then + PHOTILS.tags[#PHOTILS.tags + 1] = splitted[1] + PHOTILS.confidences[#PHOTILS.confidences+1] = splitted[2] + end + end + + dt.print(string.format(_("%s found %d tags for your image"), MODULE_NAME, + #PHOTILS.tags)) + os.remove(tmp_file) + + return true +end + +function PHOTILS.on_tags_clicked() + PHOTILS.page = 1 + GUI.warning.label = "" + + PHOTILS.on_reset(false) + + local images = dt.gui.selection() + + if #images == 0 then + 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.gui.selection({images[1]}) + dt.control.sleep(2000) + end + + with_export = dt.preferences.read(MODULE_NAME, "export_image_before_for_tags", "bool") + if not PHOTILS.get_tags(images[1], with_export) then + local msg = string.format(_("%s failed, see terminal output for details"), MODULE_NAME) + GUI.warning_label.label = msg + GUI.stack.active = GUI.error_view + dt.print(msg) + return + end + + if #PHOTILS.tags == 0 then + local msg = string.format(_("no tags were found"), MODULE_NAME) + GUI.warning_label.label = msg + GUI.stack.active = GUI.error_view + return + end + + GUI.stack.active = GUI.tag_view + PHOTILS.paginate() + end +end + +function PHOTILS.tag_selected(tag_button) + if PHOTILS.in_pagination then return end + + if tag_button.value then + local tag = tag_button.label + if dt.preferences.read(MODULE_NAME, "show_confidence", "bool") then + local idx = string.find(tag, "%(") - 2 + tag = string.sub(tag, 0, idx) + end + + PHOTILS.selected_tags[tag] = tag + else + PHOTILS.selected_tags[tag_button.label] = nil + end + + local num_selected = num_keys(PHOTILS.selected_tags) + if num_selected == 0 then + GUI.attach_button.label = "" + GUI.attach_button.sensitive = false + else + GUI.attach_button.label = string.format(_("attach %d tags"), + num_selected) + GUI.attach_button.sensitive = true + end +end + +function PHOTILS.on_reset(with_view) + if with_view then GUI.stack.active = 1 end + + for k, _ in pairs(PHOTILS.selected_tags) do + PHOTILS.selected_tags[k] = nil + end + + for _, v in ipairs(GUI.tag_box) do + v.value = false + end + + GUI.attach_button.label = "" + GUI.attach_button.sensitive = false +end + +local function install_module() + if not PHOTILS.module_installed then + dt.register_lib(MODULE_NAME, + _("photils auto-tagger"), + true, + true, + PHOTILS.plugin_display_views, + GUI.container, + nil, + nil + ) + PHOTILS.module_installed = true + end +end + +local function destroy() + dt.gui.libs[MODULE_NAME].visible = false + dt.destroy_event("photils", "mouse-over-image-changed") +end + +local function restart() + dt.gui.libs[MODULE_NAME].visible = true + dt.register_event("photils", "mouse-over-image-changed", + PHOTILS.image_changed) +end + +local function show() + dt.gui.libs[MODULE_NAME].visible = true +end + + +-- add a fix number of buttons +for _ = 1, PHOTILS.per_page, 1 do + local btn_tag = dt.new_widget("check_button") { + label = "", + sensitive = false, + clicked_callback = PHOTILS.tag_selected + } + + table.insert(GUI.tag_box, btn_tag) +end + +if not photils_installed then + GUI.warning_label.label = _("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.") +end + +GUI.pagination = dt.new_widget("box") { + orientation = "horizontal", + GUI.prev_button, + GUI.page_label, + GUI.next_button +} + + +table.insert(GUI.error_view, GUI.warning_label) +if not photils_installed then + table.insert(GUI.error_view, df.executable_path_widget({"photils-cli"})) + table.insert(GUI.error_view, GUI.restart_required_label) +end +table.insert(GUI.stack, GUI.error_view) +table.insert(GUI.stack, GUI.tag_view) + +table.insert(GUI.tag_view, GUI.pagination) +table.insert(GUI.tag_view, GUI.tag_box) +table.insert(GUI.tag_view, GUI.attach_button) +table.insert(GUI.tag_view, GUI.warning) + +table.insert(GUI.container, GUI.confidence_slider) +table.insert(GUI.container, GUI.stack) + +GUI.stack.active = 1 + + + +-- uses photils: prefix because script settings are all together and not seperated by script +dt.preferences.register(MODULE_NAME, + "show_confidence", + "bool", + _("photils: show confidence value"), + _("if enabled, the confidence value for each tag is displayed"), + true) + +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."), + true) + +dt.register_event("photils", "mouse-over-image-changed", + PHOTILS.image_changed) + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not PHOTILS.event_registered then + dt.register_event( + "photils", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + PHOTILS.event_registered = true + end +end + +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = show + +return script_data diff --git a/contrib/quicktag.lua b/contrib/quicktag.lua index c5ade264..c218f09c 100644 --- a/contrib/quicktag.lua +++ b/contrib/quicktag.lua @@ -44,20 +44,37 @@ USAGE local dt = require "darktable" +local du = require "lib/dtutils" local debug = require "darktable.debug" +du.check_min_api_version("7.0.0", "quicktag") -local gettext = dt.gettext - -dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0}) - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("quicktag",dt.configuration.config_dir.."/lua/locale/") +local gettext = dt.gettext.gettext local function _(msgid) - return gettext.dgettext("quicktag", 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 + +local qt = {} +qt.module_installed = false +qt.event_registered = false +qt.widget_table = {} + -- maximum length of button labels dt.preferences.register("quickTag", "labellength", @@ -155,7 +172,7 @@ abbrevate_tags(quicktag_table) local button = {} -- function to create buttons with tags as labels -for j=1,qnr do +for j = 1, qnr do button[#button+1] = dt.new_widget("button") { label = j..": "..quicktag_table[j], clicked_callback = function() tagattach(tostring(quicktag_table[j]),j) end} @@ -176,12 +193,56 @@ local function update_quicktag_list() end end +local function install_module() + if not qt.module_installed then + dt.register_lib( + "quicktag", -- Module name + _("quick tag"), -- name + true, -- expandable + false, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 490}}, + + dt.new_widget("box"){ + orientation = "vertical", + table.unpack(qt.widget_table), + + }, + nil,-- view_enter + nil -- view_leave + ) + qt.module_installed = true + end +end + +local function destroy() + dt.gui.libs["quicktag"].visible = false + for i=1,qnr do + dt.destroy_event("quicktag " .. tostring(i), "shortcut") + end +end + +local function restart() + dt.gui.libs["quicktag"].visible = true + for i = 1 ,qnr do + dt.register_event("quicktag", "shortcut", + function(event, shortcut) tagattach(tostring(quicktag_table[i])) end, + string.format(_("quicktag %i"),i)) + end +end + +local function show() + dt.gui.libs["quicktag"].visible = true +end + + + + update_quicktag_list() local new_quicktag = dt.new_widget("entry"){ text = "", placeholder = _("new tag"), - is_password = true, + is_password = false, editable = true, tooltip = _("enter your tag here") } @@ -212,40 +273,46 @@ local new_qt_widget = dt.new_widget ("box") { -- back UI elements in a table -- thanks to wpferguson for the hint -local widget_table = {} for i=1,qnr do - widget_table[#widget_table + 1] = button[i] + qt.widget_table[#qt.widget_table + 1] = button[i] end -widget_table[#widget_table + 1] = dt.new_widget("separator"){} -widget_table[#widget_table + 1] = old_quicktag -widget_table[#widget_table + 1] = new_qt_widget +qt.widget_table[#qt.widget_table + 1] = dt.new_widget("separator"){} +qt.widget_table[#qt.widget_table + 1] = old_quicktag +qt.widget_table[#qt.widget_table + 1] = new_qt_widget --create module -dt.register_lib( - "quicktag", -- Module name - "quicktag", -- name - true, -- expandable - false, -- resetable - {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 490}}, - - dt.new_widget("box"){ - orientation = "vertical", - table.unpack(widget_table), - - }, - nil,-- view_enter - nil -- view_leave -) +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not qt.event_registered then + dt.register_event( + "quicktag", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + qt.event_registered = true + end +end -- create shortcuts for i=1,qnr do - dt.register_event("shortcut", + dt.register_event("quicktag " .. tostring(i), "shortcut", function(event, shortcut) tagattach(tostring(quicktag_table[i])) end, - string.format(_("quicktag %i"),i)) + string.format(_("quick tag %i"),i)) end +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = show + +return script_data + -- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua -- kate: tab-indents: off; indent-width 2; replace-tabs on; remove-trailing-space on; diff --git a/contrib/rate_group.lua b/contrib/rate_group.lua index 253443fd..0e9ddb29 100644 --- a/contrib/rate_group.lua +++ b/contrib/rate_group.lua @@ -39,9 +39,32 @@ ]] local dt = require "darktable" +local du = require "lib/dtutils" -- added version check -dt.configuration.check_version(...,{3,0,0},{4,0,0}) +du.check_min_api_version("7.0.0", "rate_group") + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("rate group"), + purpose = _("rate all images in a group"), + author = "Dom H (dom@hxy.io)", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/rate_group" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them local function apply_rating(rating) local images = dt.gui.action_images @@ -52,36 +75,58 @@ local function apply_rating(rating) end end if rating < 0 then - dt.print("rejecting group(s)") + dt.print(_("rejecting group(s)")) else - dt.print("applying rating " ..rating.. " to group(s)") + dt.print(string.format(_("applying rating %d to group(s)"), rating)) end end -dt.register_event("shortcut",function(event, shortcut) +local function destroy() + dt.destroy_event("rg_reject", "shortcut") + dt.destroy_event("rg0", "shortcut") + dt.destroy_event("rg1", "shortcut") + dt.destroy_event("rg2", "shortcut") + dt.destroy_event("rg3", "shortcut") + dt.destroy_event("rg4", "shortcut") + dt.destroy_event("rg5", "shortcut") +end + +dt.register_event("rg_reject", "shortcut", + function(event, shortcut) apply_rating(-1) -end, "Reject group") +end, _("reject group")) -dt.register_event("shortcut",function(event, shortcut) +dt.register_event("rg0", "shortcut", + function(event, shortcut) apply_rating(0) -end, "Rate group 0") + end, string.format(_("rate group %d"), 0) +) -dt.register_event("shortcut",function(event, shortcut) +dt.register_event("rg1", "shortcut", + function(event, shortcut) apply_rating(1) -end, "Rate group 1") +end, string.format(_("rate group %d"), 1)) -dt.register_event("shortcut",function(event, shortcut) +dt.register_event("rg2", "shortcut", + function(event, shortcut) apply_rating(2) -end, "Rate group 2") +end, string.format(_("rate group %d"), 2)) -dt.register_event("shortcut",function(event, shortcut) +dt.register_event("rg3", "shortcut", + function(event, shortcut) apply_rating(3) -end, "Rate group 3") +end, string.format(_("rate group %d"), 3)) -dt.register_event("shortcut",function(event, shortcut) +dt.register_event("rg4", "shortcut", + function(event, shortcut) apply_rating(4) -end, "Rate group 4") +end, string.format(_("rate group %d"), 4)) -dt.register_event("shortcut",function(event, shortcut) +dt.register_event("rg5", "shortcut", + function(event, shortcut) apply_rating(5) -end, "Rate group 5") +end, string.format(_("rate group %d"), 5)) + +script_data.destroy = destroy + +return script_data diff --git a/contrib/rename-tags.lua b/contrib/rename-tags.lua index 69c6e0a8..ae77056a 100644 --- a/contrib/rename-tags.lua +++ b/contrib/rename-tags.lua @@ -1,3 +1,7 @@ +--[[ + Copyright (c) 2016 Sebastian Witt + Copyright (c) 2018 Bill Ferguson +]] --[[ Rename tags @@ -18,39 +22,79 @@ USAGE LICENSE GPLv2 +Changes +* 20180912 - Did an RTFM and read a bug (12277, thanks Christian G) that showed + a way to get the images containing the old tag one by one instead of searching + the entire database. Changed rename_tags() function to use this method. ]] local darktable = require "darktable" +local du = require "lib/dtutils" local debug = require "darktable.debug" +-- check API version +du.check_min_api_version("7.0.0", "rename-tags") +du.deprecated("contrib/rename-tags.lua","darktable release 4.0") + +local gettext = darktable.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("rename tags"), + purpose = _("rename an existing tag"), + author = "Sebastian Witt (se.witt@gmx.net)", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/rename-tags" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +local rt = {} +rt.module_installed = false +rt.event_registered = false + -- GUI entries -local old_tag = darktable.new_widget("entry") { tooltip = "Enter old tag" } -local new_tag = darktable.new_widget("entry") { tooltip = "Enter new tag" } +local old_tag = darktable.new_widget("entry") { tooltip = _("enter old tag") } +local new_tag = darktable.new_widget("entry") { tooltip = _("enter new tag") } + +local function rename_reset() + old_tag.text = '' + new_tag.text = '' +end -- This function does the renaming -local function rename_tags(widget) +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 - local Count = 0 + local count = 0 -- Check if old tag exists 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 @@ -58,57 +102,87 @@ local function rename_tags(widget) -- Create if it does not exists local nt = darktable.tags.create (new_tag.text) - -- Search images for old tag - dbcount = #darktable.database - for i,image in ipairs(darktable.database) do + -- Get number of images for old tag + local dbcount = #ot + + -- loop through the images containing the old tag, and attach the new tag + for i=1, #ot do -- Update progress bar job.percent = i / dbcount - - local tags = image:get_tags () - for _,t in ipairs (tags) do - if t.name == old_tag.text then - -- Found it, attach new tag - image:attach_tag (nt) - Count = Count + 1 - end - end + ot[i]:attach_tag(nt) + count = count + 1 end -- Delete old tag, this removes it from all images darktable.tags.delete (ot) job.valid = false - darktable.print ("Renamed tags for " .. Count .. " images") + darktable.print (string.format(_("renamed tags for %d images"), count)) old_tag.editable = true new_tag.editable = true + + -- reset the gui fields + + rename_reset() +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) + rt.module_installed = true + end +end + +local function destroy() + darktable.gui.libs["rename_tags"].visible = false +end + +local function restart() + darktable.gui.libs["rename_tags"].visible = true 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 } -local function rename_reset(widget) - old_tag.text = '' - new_tag.text = '' -end - -local rename_widget = darktable.new_widget ("box") { +rt.rename_widget = darktable.new_widget ("box") { orientation = "vertical", 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 + install_module() +else + if not rt.event_registered then + darktable.register_event( + "rename_tags", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + rt.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 -darktable.register_lib ("rename_tags", "rename tag", true, true, {[darktable.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 20},}, rename_widget, nil, nil) diff --git a/contrib/rename_images.lua b/contrib/rename_images.lua new file mode 100644 index 00000000..0093c540 --- /dev/null +++ b/contrib/rename_images.lua @@ -0,0 +1,235 @@ +--[[ + + rename.lua - rename image file(s) + + Copyright (C) 2020, 2021 Bill Ferguson . + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +]] +--[[ + rename - rename an image file or files + + This shortcut resets the GPS information to that contained within + the image file. If no GPS info is in the image file, the GPS data + is cleared. + + USAGE + * require this script from your luarc file or start it from script_manager + * select an image or images + * enter a renaming pattern + * click the button to rename the files + + BUGS, COMMENTS, SUGGESTIONS + * Send to Bill Ferguson, wpferguson@gmail.com + + CHANGES + + TODO + * Add pattern builder + * Add new name preview +]] + +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 function _(msgid) + return gettext(msgid) +end + +-- namespace variable +local rename = { + presets = {}, + widgets = {}, +} +rename.module_installed = false +rename.event_registered = false + +-- script_manager integration +local script_data = {} + +script_data.metadata = { + name = _("rename images"), + purpose = _("rename an image file or files"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/rename_images" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +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 DESKTOP = HOME .. PS .. "Desktop" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function stop_job(job) + job.valid = false +end + +local function install_module() + if not rename.module_installed then + dt.register_lib( + MODULE_NAME, + _("rename images"), + true, + true, + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER",700}}, + dt.new_widget("box"){ + orientation = "vertical", + rename.widgets.pattern, + rename.widgets.button, + }, + nil, + nil + ) + rename.module_installed = true + end +end + +local function destroy() + dt.gui.libs[MODULE_NAME].visible = false +end + +local function restart() + dt.gui.libs[MODULE_NAME].visible = true +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N +-- - - - - - - - - - - - - - - - - - - - - - - - + +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) + if string.len(pattern) > 0 then + local datetime = os.date("*t") + + local job = dt.gui.create_job(_("renaming images"), true, stop_job) + 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) + if new_name == -1 then + dt.print(_("unable to do variable substitution, exiting...")) + stop_job(job) + return + end + ds.clear_substitute_list() + local args = {} + local path = string.sub(df.get_path(new_name), 1, -2) + if string.len(path) == 0 then + path = image.path + end + local filename = df.get_filename(new_name) + local filmname = image.path + if path ~= image.path then + if not df.check_if_file_exists(df.sanitize_filename(path)) then + df.mkdir(df.sanitize_filename(path)) + end + filmname = path + end + args[1] = dt.films.new(filmname) + args[2] = image + if filename ~= image.filename then + args[3] = filename + end + dt.database.move_image(table.unpack(args)) + end + end + stop_job(job) + local collect_rules = dt.gui.libs.collect.filter() + dt.gui.libs.collect.filter(collect_rules) + dt.gui.views.lighttable.set_image_visible(first_image) + dt.print(string.format(_("renamed %d images"), #images)) + else -- pattern length + dt.print_error("no pattern supplied, returning...") + dt.print(_("please enter the new name or pattern")) + end + else -- image count + dt.print_error("no images selected, returning...") + dt.print(_("please select some images and try again")) + 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 = "" +} + +local pattern_pref = dt.preferences.read(MODULE_NAME, "pattern", "string") +if pattern_pref then + rename.widgets.pattern.text = pattern_pref +end + +rename.widgets.button = dt.new_widget("button"){ + label = _("rename"), + clicked_callback = function(this) + do_rename(dt.gui.action_images) + end +} + +if dt.gui.current_view().id == "lighttable" then + install_module() +else + if not rename.event_registered then + dt.register_event( + "rename_images", "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + rename.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 diff --git a/contrib/select_non_existing.lua b/contrib/select_non_existing.lua new file mode 100644 index 00000000..b47c7cf6 --- /dev/null +++ b/contrib/select_non_existing.lua @@ -0,0 +1,84 @@ +--[[ + This file is part of darktable, + copyright (c) 2023 Dirk Dittmar + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +]] +--[[ +Enable selection of non-existing images in the the currently worked on images, e.g. the ones selected by the collection module. +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" + +-- module name +local MODULE = "select_non_existing" + +du.check_min_api_version("9.1.0", MODULE) + +-- figure out the path separator +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +local function stop_job(job) + job.valid = false +end + +local function select_nonexisting_images(event, images) + local selection = {} + + local job = dt.gui.create_job(_("select non existing images"), true, stop_job) + for key,image in ipairs(images) do + if(job.valid) then + job.percent = (key - 1)/#images + local filepath = image.path..PS..image.filename + local file_exists = df.test_file(filepath, "e") + dt.print_log(filepath.." exists? => "..tostring(file_exists)) + if (not file_exists) then + table.insert(selection, image) + end + else + break + end + end + stop_job(job) + + return selection +end + +local function destroy() + dt.gui.libs.select.destroy_selection(MODULE) +end + +dt.gui.libs.select.register_selection( + MODULE, + _("select non existing"), + select_nonexisting_images, + _("select all non-existing images in the current images")) + +local script_data = {} + +script_data.metadata = { + name = _("select non existing"), + purpose = _("enable selection of non-existing images"), + author = "Dirk Dittmar", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/select_non_existing" +} + +script_data.destroy = destroy +return script_data \ No newline at end of file diff --git a/contrib/select_untagged.lua b/contrib/select_untagged.lua index 9e504c0d..0ca4b5d2 100644 --- a/contrib/select_untagged.lua +++ b/contrib/select_untagged.lua @@ -19,38 +19,52 @@ Enable selection of untagged images (darktable|* tags are ignored) ]] local dt = require "darktable" -local gettext = dt.gettext +local du = require "lib/dtutils" +local gettext = dt.gettext.gettext -dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0}) - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("select_untagged",dt.configuration.config_dir.."/lua/locale/") +du.check_min_api_version("7.0.0", "select_untagged") local function _(msgid) - return gettext.dgettext("select_untagged", 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 + local function stop_job(job) job.valid = false end -local function select_untagged_images() +local function select_untagged_images(event, images) job = dt.gui.create_job(_("select untagged images"), true, stop_job) local selection = {} - for key,image in ipairs(dt.collection) do + for key,image in ipairs(images) do if(job.valid) then - job.percent = (key-1)/#dt.collection + job.percent = (key - 1)/#images local tags = dt.tags.get_tags(image) local hasTags = false for _,tag in ipairs(tags) do - if not string.match(tag.name,"darktable|") then + if not string.match(tag.name, "darktable|") then hasTags = true end end if hasTags == false then - table.insert(selection,image) + table.insert(selection, image) end else break @@ -58,7 +72,19 @@ local function select_untagged_images() end job.valid = false - dt.gui.selection(selection) + -- return table of images to set the selection to + return selection end -dt.gui.libs.select.register_selection(_("select untagged"),select_untagged_images,_("select all images containing no tags or only tags added by darktable")) +local function destroy() + dt.gui.libs.select.destroy_selection("select_untagged") +end + +dt.gui.libs.select.register_selection( + "select_untagged", _("select untagged"), + select_untagged_images, + _("select all images containing no tags or only tags added by darktable")) + +script_data.destroy = destroy + +return script_data diff --git a/contrib/slideshowMusic.lua b/contrib/slideshowMusic.lua index f96383d2..bed3da6f 100644 --- a/contrib/slideshowMusic.lua +++ b/contrib/slideshowMusic.lua @@ -25,19 +25,32 @@ USAGE ]] local dt = require "darktable" +local du = require "lib/dtutils" local df = require "lib/dtutils.file" -require "official/yield" -local gettext = dt.gettext +local gettext = dt.gettext.gettext -dt.configuration.check_version(...,{2,0,2},{3,0,0},{4,0,0}) - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("slideshowMusic",dt.configuration.config_dir.."/lua/locale/") +du.check_min_api_version("7.0.0", "slideshowMusic") local function _(msgid) - return gettext.dgettext("slideshowMusic", 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 + local function playSlideshowMusic(_, old_view, new_view) local filename, playMusic @@ -45,7 +58,7 @@ local function playSlideshowMusic(_, old_view, new_view) playMusic = dt.preferences.read("slideshowMusic","PlaySlideshowMusic","bool") if not df.check_if_bin_exists("rhythmbox-client") then - dt.print_error(_("rhythmbox-client not found")) + dt.print_error("rhythmbox-client not found") return end @@ -61,19 +74,30 @@ 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 end +function destroy() + dt.destroy_event("slideshow_music", "view-changed") + dt.preferences.destroy("slideshowMusic", "SlideshowMusic") + dt.preferences.destroy("slideshowMusic", "PlaySlideshowMusic") +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("view-changed",playSlideshowMusic) +dt.register_event("slideshow_music", "view-changed", + playSlideshowMusic) + +script_data.destroy = destroy + +return script_data diff --git a/contrib/transfer_hierarchy.lua b/contrib/transfer_hierarchy.lua new file mode 100755 index 00000000..dd9c2708 --- /dev/null +++ b/contrib/transfer_hierarchy.lua @@ -0,0 +1,408 @@ +--[[ + TRANSFER HIERARCHY + Allows the moving or copying of images from one directory + tree to another, while preserving the existing hierarchy. + + AUTHOR + August Schwerdfeger (august@schwerdfeger.name) + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + None. + + USAGE + darktable's native operations for moving and copying images in + batches allow only one directory to be specified as the destination + for each batch. Those wanting to move or copy images from a _hierarchy_ + of directories within darktable while preserving the directory structure, + must take the laborious step of performing the operation one individual + directory at a time. + + This module allows the intact moving and copying of whole directory trees. + It was designed for the specific use case of rapidly transferring images + from a customary source (e.g., a staging directory on the local disk) + to a customary destination (e.g., a directory on a NAS device). + + Instructions for operation: + + 1. Select the set of images you want to copy. + + 2. Click the "calculate" button. This will calculate the + lowest directory in the hierarchy that contains every selected + file (i.e., the common prefix of all the images' pathnames), and + write its path into the "existing root" text box. + + 3. If (a) you have specified the "customary source root" and "customary + destination root" preferences, and (b) the selected images are all + contained under the directory specified as the customary source + root, then the "root of destination" text box will also be + automatically filled out. + + For example, suppose that you have specified '/home/user/Staging' + as your customary source root and '/mnt/storage' as your customary + destination root. If all selected images fell under the directory + '/home/user/Staging/2020/Roll0001', the "root of destination" would + be automatically filled out with '/mnt/storage/2020/Roll0001'. + + But if all selected images fall under a directory outside the + specified customary source root (e.g., '/opt/other'), the "root + of destination" text box must be filled out manually. + + It is also possible to edit the "root of destination" further once + it has been automatically filled out. + + 4. Click the "move" or "copy" button. + + Before moving or copying any images, the module will first + replicate the necessary directory hierarchy by creating all + destination directories that do not already exist; should a + directory creation attempt fail, the operation will be + aborted, but any directories already created will not be + removed. + + During the actual move/copy operation, the module transfers an + image by taking its path and replacing the string in the "existing + root" text box with that in the "root of destination" text box + (e.g., '/home/user/Staging/2020/Roll0001/DSC_0001.jpg' would be + transferred to '/mnt/storage/2020/Roll0001/DSC_0001.jpg'). + + LICENSE + LGPLv2+ +]] + + +-- Header material: BEGIN + +local darktable = require("darktable") +local dtutils = require("lib/dtutils") +local dtutils_file = require("lib/dtutils.file") +local dtutils_system = require("lib/dtutils.system") +local gettext = darktable.gettext.gettext + +local LIB_ID = "transfer_hierarchy" +dtutils.check_min_api_version("7.0.0", LIB_ID) + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("transfer hierarchy"), + purpose = _("allows the moving or copying of images from one directory tree to another, while preserving the existing hierarchy"), + author = "August Schwerdfeger (august@schwerdfeger.name)", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/transfer_hierarchy" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +local MKDIR_COMMAND = darktable.configuration.running_os == "windows" and "mkdir " or "mkdir -p " +local PATH_SEPARATOR = darktable.configuration.running_os == "windows" and "\\\\" or "/" +local PATH_SEGMENT_REGEX = "(" .. PATH_SEPARATOR .. "?)([^" .. PATH_SEPARATOR .. "]+)" + +unpack = unpack or table.unpack +gmatch = string.gfind or string.gmatch + +-- Header material: END + + + +-- Helper functions: BEGIN + +local th = {} +th.module_installed = false +th.event_registered = false + +local function pathExists(path) + local success, err, errno = os.rename(path, path) + if not success then + if errno == 13 then + return true + end + end + return success, err +end + +local function pathIsDirectory(path) + return pathExists(path..PATH_SEPARATOR) +end + +local function createDirectory(path) + local errorlevel = dtutils_system.external_command(MKDIR_COMMAND .. dtutils_file.sanitize_filename(path)) + if errorlevel == 0 and pathIsDirectory(path) then + return path + else + return nil + end +end + +local function install_module() + if not th.module_installed then + darktable.register_lib(LIB_ID, + _("transfer hierarchy"), true, true, { + [darktable.gui.views.lighttable] = { "DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 700 } + }, th.transfer_widget, nil, nil) + th.module_installed = true + end +end + +-- Helper functions: END + + +-- Widgets and business logic: BEGIN + +local sourceTextBox = darktable.new_widget("entry") { + tooltip = _("lowest directory containing all selected images"), + editable = false + } +sourceTextBox.reset_callback = function() sourceTextBox.text = "" end + +local destinationTextBox = darktable.new_widget("entry") { + text = "" +} +destinationTextBox.reset_callback = function() destinationTextBox.text = "" end + + + + + + + + + +local function findRootPath(films) + local commonSegments = nil + local prefix = "" + for film, _ in pairs(films) do + local path = film.path + if commonSegments == nil then + commonSegments = {} + local firstMatchIndex = string.find(path, PATH_SEGMENT_REGEX) + if firstMatchIndex ~= nil then + prefix = string.sub(path, 1, firstMatchIndex-1) + end + string.gsub(path, PATH_SEGMENT_REGEX, function(w, x) + if w ~= "" then table.insert(commonSegments, w) end + table.insert(commonSegments, x) + end) + else + local matcher = gmatch(path, PATH_SEGMENT_REGEX) + local i = 1 + while i < #commonSegments do + match, match2 = matcher() + if match == nil then + while i <= #commonSegments do + table.remove(commonSegments, #commonSegments) + end + break + elseif match ~= "" then + if commonSegments[i] ~= match then + while i <= #commonSegments do + table.remove(commonSegments, #commonSegments) + end + break + else + i = i+1 + end + end + if match2 == nil or commonSegments[i] ~= match2 then + while i <= #commonSegments do + table.remove(commonSegments, #commonSegments) + end + break + else + i = i+1 + end + end + end + end + if commonSegments == nil then + return prefix + end + if commonSegments[#commonSegments] == PATH_SEPARATOR then + table.remove(commonSegments, #commonSegments) + end + rv = prefix .. table.concat(commonSegments) + return rv +end + +local function calculateRoot() + films = {} + for _,img in ipairs(darktable.gui.action_images) do + films[img.film] = true + end + return findRootPath(films), films +end + +local function doCalculate() + local rootPath = calculateRoot() + if rootPath ~= nil then + sourceTextBox.text = rootPath + local sourceBase = darktable.preferences.read(LIB_ID, "source_base", "directory") + local destBase = darktable.preferences.read(LIB_ID, "destination_base", "directory") + if sourceBase ~= nil and sourceBase ~= "" and + destBase ~= nil and destBase ~= "" and + string.sub(rootPath, 1, #sourceBase) == sourceBase then + destinationTextBox.text = destBase .. string.sub(rootPath, #sourceBase+1) + end + end +end + +local function stopTransfer(transferJob) + transferJob.valid = false +end + +local function doTransfer(transferFunc) + rootPath, films = calculateRoot() + if rootPath ~= sourceTextBox.text then + darktable.print(_("transfer hierarchy: ERROR: existing root is out of sync -- click 'calculate' to update")) + return + end + if destinationTextBox.text == "" then + darktable.print(_("transfer hierarchy: ERROR: destination not specified")) + return + end + local sourceBase = sourceTextBox.text + local destBase = destinationTextBox.text + local destFilms = {} + 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])) + return + end + end + if not pathIsDirectory(films[film]) then + darktable.print(string.format(_("transfer hierarchy: ERROR: not a directory: %s"), films[film])) + return + end + destFilms[film] = darktable.films.new(films[film]) + if destFilms[film] == nil then + darktable.print(string.format(_("transfer hierarchy: ERROR: could not create film: %s"), film.path)) + end + end + + local srcFilms = {} + for _,img in ipairs(darktable.gui.action_images) do + 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) + job.percent = 0.0 + local pctIncrement = 1.0 / #(darktable.gui.action_images) + for _,img in ipairs(darktable.gui.action_images) do + if job.valid and img.film == srcFilms[img] then + destFilm = destFilms[img.film] + transferFunc(img, destFilm) + job.percent = job.percent + pctIncrement + end + end + job.valid = false + local filterRules = darktable.gui.libs.collect.filter() + darktable.gui.libs.collect.filter(filterRules) +end + +local function doMove() + doTransfer(darktable.database.move_image) +end + +local function doCopy() + doTransfer(darktable.database.copy_image) +end + +local function destroy() + darktable.gui.libs[LIB_ID].visible = false +end + +local function restart() + darktable.gui.libs[LIB_ID].visible = true +end + + + + + +th.transfer_widget = darktable.new_widget("box") { + orientation = "vertical", + darktable.new_widget("button") { + label = _("calculate"), + clicked_callback = doCalculate + }, + darktable.new_widget("label") { + label = _("existing root"), + halign = "start" + }, + sourceTextBox, + darktable.new_widget("label") { + label = _("root of destination"), + halign = "start" + }, + destinationTextBox, + darktable.new_widget("button") { + label = _("move"), + tooltip = "Move all selected images", + clicked_callback = doMove + }, + darktable.new_widget("button") { + label = _("copy"), + tooltip = _("copy all selected images"), + clicked_callback = doCopy + } +} + +-- Widgets and business logic: END + + + + + + +-- Preferences: BEGIN + +darktable.preferences.register( + LIB_ID, + "source_base", + "string", + "[transfer hierarchy] Customary source root", + "", + "") + +darktable.preferences.register( + LIB_ID, + "destination_base", + "string", + "[transfer hierarchy] Customary destination root", + "", + "") + +-- Preferences: END + +if darktable.gui.current_view().id == "lighttable" then + install_module() +else + if not th.event_registered then + darktable.register_event( + LIB_ID, "view-changed", + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then + install_module() + end + end + ) + th.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 diff --git a/contrib/ultrahdr.lua b/contrib/ultrahdr.lua new file mode 100644 index 00000000..e5562324 --- /dev/null +++ b/contrib/ultrahdr.lua @@ -0,0 +1,1066 @@ +--[[ + + UltraHDR image generation for darktable + + copyright (c) 2024 Krzysztof Kotowicz + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . + +]] --[[ + +ULTRAHDR +Generate UltraHDR JPEG images from various combinations of source files (SDR, HDR, gain map). + +https://developer.android.com/media/platform/hdr-image-format + +The images are merged using libultrahdr example application (ultrahdr_app). + +ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT +* ultrahdr_app (built using https://github.com/google/libultrahdr/blob/main/docs/building.md instructions) +* exiftool +* ffmpeg + +USAGE +* require this file from your main luarc config file +* set binary tool paths +* Use UltraHDR module to generate UltraHDR images from selection + +]] local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local ds = require "lib/dtutils.string" +local log = require "lib/dtutils.log" +local dtsys = require "lib/dtutils.system" +local dd = require "lib/dtutils.debug" +local gettext = dt.gettext.gettext + +local namespace = "ultrahdr" + +local LOG_LEVEL = log.info + +-- works with darktable API version from 4.8.0 on +du.check_min_api_version("9.3.0", "ultrahdr") + +local function _(msgid) + return gettext(msgid) +end + +local job + +local GUI = { + optionwidgets = { + settings_label = {}, + encoding_variant_combo = {}, + selection_type_combo = {}, + encoding_settings_box = {}, + output_settings_label = {}, + output_settings_box = {}, + output_filepath_label = {}, + output_filepath_widget = {}, + overwrite_on_conflict = {}, + copy_exif = {}, + import_to_darktable = {}, + min_content_boost = {}, + max_content_boost = {}, + hdr_capacity_min = {}, + hdr_capacity_max = {}, + metadata_label = {}, + metadata_box = {}, + edit_executables_button = {}, + executable_path_widget = {}, + quality_widget = {}, + gainmap_downsampling_widget = {}, + target_display_peak_nits_widget = {} + }, + options = {}, + run = {} +} + +local flags = {} +flags.event_registered = false -- keep track of whether we've added an event callback or not +flags.module_installed = false -- keep track of whether the module is module_installed + +local script_data = {} + +script_data.metadata = { + name = _("UltraHDR"), + purpose = _("generate UltraHDR images"), + author = "Krzysztof Kotowicz" +} + +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +local ENCODING_VARIANT_SDR_AND_GAINMAP = 1 +local ENCODING_VARIANT_SDR_AND_HDR = 2 +local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3 +local ENCODING_VARIANT_HDR_ONLY = 4 + +local SELECTION_TYPE_ONE_STACK = 1 +local SELECTION_TYPE_GROUP_BY_FNAME = 2 + +-- Values are defined in darktable/src/common/colorspaces.h +local DT_COLORSPACE_PQ_P3 = 24 +local DT_COLORSPACE_DISPLAY_P3 = 26 + +-- 1-based position of a colorspace in export profile combobox. +local COLORSPACE_TO_GUI_ACTION = { + [DT_COLORSPACE_PQ_P3] = 9, + [DT_COLORSPACE_DISPLAY_P3] = 11 +} + +local UI_SLEEP_MS = 50 -- How many ms to sleep after UI action. + +local function set_log_level(level) + local old_log_level = log.log_level() + log.log_level(level) + return old_log_level +end + +local function restore_log_level(level) + log.log_level(level) +end + +local function generate_metadata_file(settings) + local old_log_level = set_log_level(LOG_LEVEL) + local metadata_file_fmt = [[--maxContentBoost %f +--minContentBoost %f +--gamma 1.0 +--offsetSdr 0.0 +--offsetHdr 0.0 +--hdrCapacityMin %f +--hdrCapacityMax %f]] + + local filename = df.create_unique_filename(settings.tmpdir .. PS .. "metadata.cfg") + local f, err = io.open(filename, "w+") + if not f then + dt.print(err) + return nil + end + local content = string.format(metadata_file_fmt, settings.metadata.max_content_boost, + settings.metadata.min_content_boost, settings.metadata.hdr_capacity_min, settings.metadata.hdr_capacity_max) + f:write(content) + f:close() + restore_log_level(old_log_level) + return filename +end + +local function save_preferences() + local old_log_level = set_log_level(LOG_LEVEL) + dt.preferences.write(namespace, "encoding_variant", "integer", GUI.optionwidgets.encoding_variant_combo.selected) + dt.preferences.write(namespace, "selection_type", "integer", GUI.optionwidgets.selection_type_combo.selected) + dt.preferences.write(namespace, "output_filepath_pattern", "string", GUI.optionwidgets.output_filepath_widget.text) + dt.preferences.write(namespace, "overwrite_on_conflict", "bool", GUI.optionwidgets.overwrite_on_conflict.value) + dt.preferences.write(namespace, "import_to_darktable", "bool", GUI.optionwidgets.import_to_darktable.value) + dt.preferences.write(namespace, "copy_exif", "bool", GUI.optionwidgets.copy_exif.value) + if GUI.optionwidgets.min_content_boost.value then + dt.preferences.write(namespace, "min_content_boost", "float", GUI.optionwidgets.min_content_boost.value) + dt.preferences.write(namespace, "max_content_boost", "float", GUI.optionwidgets.max_content_boost.value) + dt.preferences.write(namespace, "hdr_capacity_min", "float", GUI.optionwidgets.hdr_capacity_min.value) + dt.preferences.write(namespace, "hdr_capacity_max", "float", GUI.optionwidgets.hdr_capacity_max.value) + end + dt.preferences.write(namespace, "quality", "integer", GUI.optionwidgets.quality_widget.value) + dt.preferences.write(namespace, "gainmap_downsampling", "integer", + GUI.optionwidgets.gainmap_downsampling_widget.value) + dt.preferences.write(namespace, "target_display_peak_nits", "integer", + (GUI.optionwidgets.target_display_peak_nits_widget.value + 0.5) // 1) + restore_log_level(old_log_level) +end + +local function default_to(value, default) + if value == 0 or value == "" then + return default + end + return value +end + +local function load_preferences() + local old_log_level = set_log_level(LOG_LEVEL) + -- Since the option #1 is the default, and empty numeric prefs are 0, we can use math.max + GUI.optionwidgets.encoding_variant_combo.selected = math.max( + dt.preferences.read(namespace, "encoding_variant", "integer"), ENCODING_VARIANT_SDR_AND_GAINMAP) + GUI.optionwidgets.selection_type_combo.selected = math.max( + dt.preferences.read(namespace, "selection_type", "integer"), SELECTION_TYPE_ONE_STACK) + + GUI.optionwidgets.output_filepath_widget.text = default_to(dt.preferences.read(namespace, "output_filepath_pattern", "string"), + "$(FILE_FOLDER)/$(FILE_NAME)_ultrahdr") + GUI.optionwidgets.overwrite_on_conflict.value = dt.preferences.read(namespace, "overwrite_on_conflict", "bool") + GUI.optionwidgets.import_to_darktable.value = dt.preferences.read(namespace, "import_to_darktable", "bool") + GUI.optionwidgets.copy_exif.value = dt.preferences.read(namespace, "copy_exif", "bool") + GUI.optionwidgets.min_content_boost.value = default_to(dt.preferences.read(namespace, "min_content_boost", "float"), + 1.0) + GUI.optionwidgets.max_content_boost.value = default_to(dt.preferences.read(namespace, "max_content_boost", "float"), + 6.0) + GUI.optionwidgets.hdr_capacity_min.value = default_to(dt.preferences.read(namespace, "hdr_capacity_min", "float"), + 1.0) + GUI.optionwidgets.hdr_capacity_max.value = default_to(dt.preferences.read(namespace, "hdr_capacity_max", "float"), + 6.0) + GUI.optionwidgets.quality_widget.value = default_to(dt.preferences.read(namespace, "quality", "integer"), 95) + GUI.optionwidgets.target_display_peak_nits_widget.value = default_to( + dt.preferences.read(namespace, "target_display_peak_nits", "integer"), 10000) + GUI.optionwidgets.gainmap_downsampling_widget.value = default_to( + dt.preferences.read(namespace, "gainmap_downsampling", "integer"), 0) + restore_log_level(old_log_level) +end + +local function set_profile(colorspace) + local set_directly = true + + if set_directly then + -- New method, with hardcoded export profile values. + local old = dt.gui.action("lib/export/profile", 0, "selection", "", "") * -1 + local new = COLORSPACE_TO_GUI_ACTION[colorspace] or colorspace + log.msg(log.debug, string.format("Changing export profile from %d to %d", old, new)) + dt.gui.action("lib/export/profile", 0, "selection", "next", new - old) + dt.control.sleep(UI_SLEEP_MS) + return old + else + -- Old method + return set_combobox("lib/export/profile", 0, "plugins/lighttable/export/icctype", colorspace) + end +end + +-- Changes the combobox selection blindly until a paired config value is set. +-- Workaround for https://github.com/darktable-org/lua-scripts/issues/522 +local function set_combobox(path, instance, config_name, new_config_value) + local old_log_level = set_log_level(LOG_LEVEL) + local pref = dt.preferences.read("darktable", config_name, "integer") + if pref == new_config_value then + return new_config_value + end + + dt.gui.action(path, 0, "selection", "first", 1.0) + dt.control.sleep(UI_SLEEP_MS) + local limit, i = 30, 0 -- in case there is no matching config value in the first n entries of a combobox. + while i < limit do + i = i + 1 + dt.gui.action(path, 0, "selection", "next", 1.0) + dt.control.sleep(UI_SLEEP_MS) + if dt.preferences.read("darktable", config_name, "integer") == new_config_value then + log.msg(log.debug, string.format("Changed %s from %d to %d", config_name, pref, new_config_value)) + return pref + end + end + log.msg(log.error, string.format("Could not change %s from %d to %d", config_name, pref, new_config_value)) + restore_log_level(old_log_level) +end + +local function assert_settings_correct(encoding_variant) + local old_log_level = set_log_level(LOG_LEVEL) + local errors = {} + local settings = { + bin = { + ultrahdr_app = df.check_if_bin_exists("ultrahdr_app"), + exiftool = df.check_if_bin_exists("exiftool"), + ffmpeg = df.check_if_bin_exists("ffmpeg") + }, + overwrite_on_conflict = GUI.optionwidgets.overwrite_on_conflict.value, + output_filepath_pattern = GUI.optionwidgets.output_filepath_widget.text, + import_to_darktable = GUI.optionwidgets.import_to_darktable.value, + copy_exif = GUI.optionwidgets.copy_exif.value, + metadata = { + min_content_boost = GUI.optionwidgets.min_content_boost.value, + max_content_boost = GUI.optionwidgets.max_content_boost.value, + hdr_capacity_min = GUI.optionwidgets.hdr_capacity_min.value, + hdr_capacity_max = GUI.optionwidgets.hdr_capacity_max.value + }, + quality = GUI.optionwidgets.quality_widget.value, + target_display_peak_nits = (GUI.optionwidgets.target_display_peak_nits_widget.value + 0.5) // 1, + downsample = 2 ^ GUI.optionwidgets.gainmap_downsampling_widget.value, + tmpdir = dt.configuration.tmp_dir, + skip_cleanup = false, -- keep temporary files around, for debugging. + force_export = true -- if false, will copy source files instead of exporting if the file extension matches the format expectation. + } + + for k, v in pairs(settings.bin) do + if not v then + table.insert(errors, string.format(_("%s binary not found"), k)) + end + end + + if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then + if settings.metadata.min_content_boost >= settings.metadata.max_content_boost then + table.insert(errors, _("min_content_boost should not be greater than max_content_boost")) + end + if settings.metadata.hdr_capacity_min >= settings.metadata.hdr_capacity_max then + table.insert(errors, _("hdr_capacity_min should not be greater than hdr_capacity_max")) + end + end + restore_log_level(old_log_level) + if #errors > 0 then + return nil, errors + end + return settings, nil +end + +local function get_dimensions(image) + if image.final_width > 0 then + return image.final_width, image.final_height + end + return image.width, image.height +end + +local function get_stacks(images, encoding_variant, selection_type) + local old_log_level = set_log_level(LOG_LEVEL) + local stacks = {} + local primary = "sdr" + local extra + if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then + extra = "gainmap" + elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then + extra = "hdr" + elseif encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then + extra = nil + elseif encoding_variant == ENCODING_VARIANT_HDR_ONLY then + extra = nil + primary = "hdr" + end + + local tags = nil + -- Group images into (primary [,extra]) stacks + -- Assume that the first encountered image from each stack is a primary one, unless it has a tag matching the expected extra_image_type, or has the expected extension + for k, v in pairs(images) do + local is_extra = false + tags = dt.tags.get_tags(v) + for ignore, tag in pairs(tags) do + if extra and tag.name == extra then + is_extra = true + end + end + if extra_image_extension and df.get_filetype(v.filename) == extra_image_extension then + is_extra = true + end + -- We assume every image in the stack is generated from the same source image file + local key + if selection_type == SELECTION_TYPE_GROUP_BY_FNAME then + key = df.chop_filetype(v.path .. PS .. v.filename) + elseif selection_type == SELECTION_TYPE_ONE_STACK then + key = "the_one_and_only" + end + if stacks[key] == nil then + stacks[key] = {} + end + if extra and (is_extra or stacks[key][primary]) then + -- Don't overwrite existing entries + if not stacks[key][extra] then + stacks[key][extra] = v + end + elseif not is_extra then + -- Don't overwrite existing entries + if not stacks[key][primary] then + stacks[key][primary] = v + end + end + end + -- remove invalid stacks + local count = 0 + for k, v in pairs(stacks) do + if extra then + if not v[primary] or not v[extra] then + stacks[k] = nil + else + local sdr_w, sdr_h = get_dimensions(v[primary]) + local extra_w, extra_h = get_dimensions(v[extra]) + if (sdr_w ~= extra_w) or (sdr_h ~= extra_h) then + stacks[k] = nil + end + end + end + if stacks[k] then + count = count + 1 + end + end + restore_log_level(old_log_level) + return stacks, count +end + +local function stop_job(job) + job.valid = false +end + +local function file_size(path) + local f, err = io.open(path, "r") + if not f then + return 0 + end + local size = f:seek("end") + f:close() + return size +end + +local function generate_ultrahdr(encoding_variant, images, settings, step, total_steps) + local old_log_level = set_log_level(LOG_LEVEL) + local total_substeps + local substep = 0 + local best_source_image + local uhdr + local errors = {} + local remove_files = {} + local ok + local cmd + + local function execute_cmd(cmd, errormsg) + log.msg(log.debug, cmd) + local code = dtsys.external_command(cmd) + if errormsg and code > 0 then + table.insert(errors, errormsg) + end + return code == 0 + end + + function update_job_progress() + substep = substep + 1 + if substep > total_substeps then + log.msg(log.debug, + string.format("total_substeps count is too low for encoding_variant %d", encoding_variant)) + end + job.percent = (total_substeps * step + substep) / (total_steps * total_substeps) + end + + function copy_or_export(src_image, dest, format, colorspace, props) + -- Workaround for https://github.com/darktable-org/darktable/issues/17528 + local needs_workaround = dt.configuration.api_version_string == "9.3.0" + if not settings.force_export and df.get_filetype(src_image.filename) == df.get_filetype(dest) and + not src_image.is_altered then + return df.file_copy(src_image.path .. PS .. src_image.filename, dest) + else + local prev = set_profile(colorspace) + if not prev then + return false + end + local exporter = dt.new_format(format) + for k, v in pairs(props) do + exporter[k] = v + end + local ok = exporter:write_image(src_image, dest) + if needs_workaround then + ok = not ok + end + log.msg(log.info, string.format("Exporting %s to %s (format: %s): %s", src_image.filename, dest, format, ok)) + if prev then + set_profile(prev) + end + return ok + end + return true + end + + function cleanup() + if settings.skip_cleanup then + return false + end + for _, v in pairs(remove_files) do + os.remove(v) + end + return false + end + + if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then + total_substeps = 5 + best_source_image = images["sdr"] + -- Export/copy both SDR and gainmap to JPEGs + local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) .. + ".jpg") + table.insert(remove_files, sdr) + ok = copy_or_export(images["sdr"], sdr, "jpeg", DT_COLORSPACE_DISPLAY_P3, { + quality = settings.quality + }) + if not ok then + table.insert(errors, string.format(_("Error exporting %s to %s"), images["sdr"].filename, "jpeg")) + return cleanup(), errors + end + + local gainmap + if encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then -- SDR is also a gainmap + gainmap = sdr + else + gainmap = df.create_unique_filename(settings.tmpdir .. PS .. images["gainmap"].filename .. "_gainmap.jpg") + table.insert(remove_files, gainmap) + ok = copy_or_export(images["gainmap"], gainmap, "jpeg", DT_COLORSPACE_DISPLAY_P3, { + quality = settings.quality + }) + if not ok then + table.insert(errors, string.format(_("Error exporting %s to %s"), images["gainmap"].filename, "jpeg")) + return cleanup(), errors + end + end + log.msg(log.debug, string.format("Exported files: %s, %s", sdr, gainmap)) + update_job_progress() + -- Strip EXIFs + table.insert(remove_files, sdr .. ".noexif") + cmd = settings.bin.exiftool .. " -all= " .. df.sanitize_filename(sdr) .. " -o " .. + df.sanitize_filename(sdr .. ".noexif") + if not execute_cmd(cmd, string.format(_("Error stripping EXIF from %s"), sdr)) then + return cleanup(), errors + end + if sdr ~= gainmap then + if not execute_cmd(settings.bin.exiftool .. " -all= " .. df.sanitize_filename(gainmap) .. + " -overwrite_original", string.format(_("Error stripping EXIF from %s"), gainmap)) then + return cleanup(), errors + end + end + update_job_progress() + -- Generate metadata.cfg file + local metadata_file = generate_metadata_file(settings) + table.insert(remove_files, metadata_file) + -- Merge files + uhdr = df.chop_filetype(sdr) .. "_ultrahdr.jpg" + table.insert(remove_files, uhdr) + cmd = settings.bin.ultrahdr_app .. + string.format(" -m 0 -i %s -g %s -L %d -f %s -z %s", df.sanitize_filename(sdr .. ".noexif"), -- -i + df.sanitize_filename(gainmap), -- -g + settings.target_display_peak_nits, -- -L + df.sanitize_filename(metadata_file), -- -f + df.sanitize_filename(uhdr) -- -z + ) + if not execute_cmd(cmd, string.format(_("Error merging UltraHDR to %s"), uhdr)) then + return cleanup(), errors + end + update_job_progress() + -- Copy SDR's EXIF to UltraHDR file + if settings.copy_exif then + -- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all). + -- This might hapen e.g. when the source files are Adobe gainmap HDRs. + cmd = settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -exif " .. + df.sanitize_filename(uhdr) .. " -overwrite_original -preserve" + if not execute_cmd(cmd, string.format(_("Error adding EXIF to %s"), uhdr)) then + return cleanup(), errors + end + end + update_job_progress() + elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then + total_substeps = 6 + best_source_image = images["sdr"] + -- https://discuss.pixls.us/t/manual-creation-of-ultrahdr-images/45004/20 + -- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3 + local hdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) .. + ".jxl") + table.insert(remove_files, hdr) + ok = copy_or_export(images["hdr"], hdr, "jpegxl", DT_COLORSPACE_PQ_P3, { + bpp = 10, + quality = 100, -- lossless + effort = 1 -- we don't care about the size, the file is temporary. + }) + if not ok then + table.insert(errors, string.format(_("Error exporting %s to %s"), images["hdr"].filename, "jxl")) + return cleanup(), errors + end + update_job_progress() + -- Step 2: Export SDR to PNG + local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) .. + ".png") + table.insert(remove_files, sdr) + ok = copy_or_export(images["sdr"], sdr, "png", DT_COLORSPACE_DISPLAY_P3, { + bpp = 8 + }) + if not ok then + table.insert(errors, string.format(_("Error exporting %s to %s"), images["sdr"].filename, "png")) + return cleanup(), errors + end + uhdr = df.chop_filetype(sdr) .. "_ultrahdr.jpg" + table.insert(remove_files, uhdr) + update_job_progress() + -- Step 3: Generate libultrahdr RAW images + local sdr_raw, hdr_raw = sdr .. ".raw", hdr .. ".raw" + table.insert(remove_files, sdr_raw) + table.insert(remove_files, hdr_raw) + local sdr_w, sdr_h = get_dimensions(images["sdr"]) + local resize_cmd = "" + if sdr_h % 2 + sdr_w % 2 > 0 then -- needs resizing to even dimensions. + resize_cmd = string.format(" -vf 'crop=%d:%d:0:0' ", sdr_w - sdr_w % 2, sdr_h - sdr_h % 2) + end + local size_in_px = (sdr_w - sdr_w % 2) * (sdr_h - sdr_h % 2) + cmd = + settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(sdr) .. resize_cmd .. " -pix_fmt rgba -f rawvideo " .. + df.sanitize_filename(sdr_raw) + if not execute_cmd(cmd, string.format(_("Error generating %s"), sdr_raw)) then + return cleanup(), errors + end + cmd = settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(hdr) .. resize_cmd .. + " -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(hdr_raw) + if not execute_cmd(cmd, string.format(_("Error generating %s"), hdr_raw)) then + return cleanup(), errors + end + -- sanity check for file sizes (sometimes dt exports different size images if the files were never opened in darktable view) + if file_size(sdr_raw) ~= size_in_px * 4 or file_size(hdr_raw) ~= size_in_px * 3 then + table.insert(errors, + string.format( + _("Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first."), + images["sdr"].filename, sdr_w, sdr_h)) + return cleanup(), errors + end + update_job_progress() + cmd = settings.bin.ultrahdr_app .. + string.format( + " -m 0 -y %s -p %s -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -q %d -Q %d -L %d -D 1 -s %d -w %d -h %d -z %s", + df.sanitize_filename(sdr_raw), -- -y + df.sanitize_filename(hdr_raw), -- -p + settings.quality, -- -q + settings.quality, -- -Q + settings.target_display_peak_nits, -- -L + settings.downsample, -- -s + sdr_w - sdr_w % 2, -- w + sdr_h - sdr_h % 2, -- h + df.sanitize_filename(uhdr) -- z + ) + if not execute_cmd(cmd, string.format(_("Error merging %s"), uhdr)) then + return cleanup(), errors + end + update_job_progress() + if settings.copy_exif then + -- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all). + -- This might hapen e.g. when the source files are Adobe gainmap HDRs. + cmd = settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(sdr) .. " -exif " .. + df.sanitize_filename(uhdr) .. " -overwrite_original -preserve" + if not execute_cmd(cmd, string.format(_("Error adding EXIF to %s"), uhdr)) then + return cleanup(), errors + end + end + update_job_progress() + elseif encoding_variant == ENCODING_VARIANT_HDR_ONLY then + total_substeps = 5 + best_source_image = images["hdr"] + -- TODO: Check if exporting to JXL would be ok too. + -- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3 + local hdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) .. + ".jxl") + table.insert(remove_files, hdr) + ok = copy_or_export(images["hdr"], hdr, "jpegxl", DT_COLORSPACE_PQ_P3, { + bpp = 10, + quality = 100, -- lossless + effort = 1 -- we don't care about the size, the file is temporary. + }) + if not ok then + table.insert(errors, string.format(_("Error exporting %s to %s"), images["hdr"].filename, "jxl")) + return cleanup(), errors + end + update_job_progress() + -- Step 1: Generate raw HDR image + local hdr_raw = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) .. + ".raw") + table.insert(remove_files, hdr_raw) + local hdr_w, hdr_h = get_dimensions(images["hdr"]) + local resize_cmd = "" + if hdr_h % 2 + hdr_w % 2 > 0 then -- needs resizing to even dimensions. + resize_cmd = string.format(" -vf 'crop=%d:%d:0:0' ", hdr_w - hdr_w % 2, hdr_h - hdr_h % 2) + end + local size_in_px = (hdr_w - hdr_w % 2) * (hdr_h - hdr_h % 2) + cmd = settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(hdr) .. resize_cmd .. + " -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(hdr_raw) + if not execute_cmd(cmd, string.format(_("Error generating %s"), hdr_raw)) then + return cleanup(), errors + end + if file_size(hdr_raw) ~= size_in_px * 3 then + table.insert(errors, + string.format( + _("Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first."), + images["hdr"].filename, hdr_w, hdr_h)) + return cleanup(), errors + end + update_job_progress() + uhdr = df.chop_filetype(hdr_raw) .. "_ultrahdr.jpg" + table.insert(remove_files, uhdr) + cmd = settings.bin.ultrahdr_app .. + string.format( + " -m 0 -p %s -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -q %d -Q %d -D 1 -L %d -s %d -w %d -h %d -z %s", + df.sanitize_filename(hdr_raw), -- -p + settings.quality, -- -q + settings.quality, -- -Q + settings.target_display_peak_nits, -- -L + settings.downsample, -- s + hdr_w - hdr_w % 2, -- -w + hdr_h - hdr_h % 2, -- -h + df.sanitize_filename(uhdr) -- -z + ) + if not execute_cmd(cmd, string.format(_("Error merging %s"), uhdr)) then + return cleanup(), errors + end + update_job_progress() + if settings.copy_exif then + -- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all). + -- This might hapen e.g. when the source files are Adobe gainmap HDRs. + cmd = settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(hdr) .. " -exif " .. + df.sanitize_filename(uhdr) .. " -overwrite_original -preserve" + if not execute_cmd(cmd, string.format(_("Error adding EXIF to %s"), uhdr)) then + return cleanup(), errors + end + end + update_job_progress() + end + + local output_file = ds.substitute(best_source_image, step + 1, settings.output_filepath_pattern) .. ".jpg" + if not settings.overwrite_on_conflict then + output_file = df.create_unique_filename(output_file) + end + local output_path = ds.get_path(output_file) + df.mkdir(output_path) + ok = df.file_move(uhdr, output_file) + if not ok then + table.insert(errors, string.format(_("Error generating UltraHDR for %s"), best_source_image.filename)) + return cleanup(), errors + end + if settings.import_to_darktable then + local img = dt.database.import(output_file) + -- Add "ultrahdr" tag to the imported image + local tagnr = dt.tags.find("ultrahdr") + if tagnr == nil then + dt.tags.create("ultrahdr") + tagnr = dt.tags.find("ultrahdr") + end + dt.tags.attach(tagnr, img) + end + cleanup() + update_job_progress() + log.msg(log.info, string.format("Generated %s.", df.get_filename(output_file))) + dt.print(string.format(_("Generated %s."), df.get_filename(output_file))) + restore_log_level(old_log_level) + return true, nil +end + +local function main() + local old_log_level = set_log_level(LOG_LEVEL) + save_preferences() + + local selection_type = GUI.optionwidgets.selection_type_combo.selected + local encoding_variant = GUI.optionwidgets.encoding_variant_combo.selected + log.msg(log.info, string.format("using selection type %d, encoding variant %d", selection_type, encoding_variant)) + + local settings, errors = assert_settings_correct(encoding_variant) + if not settings then + dt.print(string.format(_("Export settings are incorrect, exiting:\n\n- %s"), table.concat(errors, "\n- "))) + return + end + + local stacks, stack_count = get_stacks(dt.gui.selection(), encoding_variant, selection_type) + if stack_count == 0 then + dt.print(string.format(_( + "No image stacks detected.\n\nMake sure that the image pairs have the same widths and heights."), + stack_count)) + return + end + dt.print(string.format(_("Detected %d image stack(s)"), stack_count)) + job = dt.gui.create_job(_("Generating UltraHDR images"), true, stop_job) + local count = 0 + local msg + for i, v in pairs(stacks) do + local ok, errors = generate_ultrahdr(encoding_variant, v, settings, count, stack_count) + if not ok then + dt.print(string.format(_("Generating UltraHDR images failed:\n\n- %s"), table.concat(errors, "\n- "))) + job.valid = false + return + end + count = count + 1 + -- sleep for a short moment to give stop_job callback function a chance to run + dt.control.sleep(10) + end + -- stop job and remove progress_bar from ui, but only if not alreay canceled + if (job.valid) then + job.valid = false + end + + log.msg(log.info, string.format("Generated %d UltraHDR image(s).", count)) + dt.print(string.format(_("Generated %d UltraHDR image(s)."), count)) + restore_log_level(old_log_level) +end + +GUI.optionwidgets.settings_label = dt.new_widget("section_label") { + label = _("UltraHDR settings") +} + +GUI.optionwidgets.output_settings_label = dt.new_widget("section_label") { + label = _("output") +} + +GUI.optionwidgets.output_filepath_label = dt.new_widget("label") { + label = _("file path pattern"), + tooltip = ds.get_substitution_tooltip() +} + +GUI.optionwidgets.output_filepath_widget = dt.new_widget("entry") { + tooltip = ds.get_substitution_tooltip(), + placeholder = _("e.g. $(FILE_FOLDER)/$(FILE_NAME)_ultrahdr") +} + +GUI.optionwidgets.overwrite_on_conflict = dt.new_widget("check_button") { + label = _("overwrite if exists"), + tooltip = _( + "If the output file already exists, overwrite it. If unchecked, a unique filename will be created instead.") +} + +GUI.optionwidgets.import_to_darktable = dt.new_widget("check_button") { + label = _("import UltraHDRs to library"), + tooltip = _("Import UltraHDR images to darktable library after generating, with an 'ultrahdr' tag attached.") +} + +GUI.optionwidgets.copy_exif = dt.new_widget("check_button") { + label = _("copy EXIF data"), + tooltip = _("Copy EXIF data into UltraHDR file(s) from their SDR sources.") +} + +GUI.optionwidgets.output_settings_box = dt.new_widget("box") { + orientation = "vertical", + GUI.optionwidgets.output_settings_label, + GUI.optionwidgets.output_filepath_label, + GUI.optionwidgets.output_filepath_widget, + GUI.optionwidgets.overwrite_on_conflict, + GUI.optionwidgets.import_to_darktable, + GUI.optionwidgets.copy_exif +} + +GUI.optionwidgets.metadata_label = dt.new_widget("label") { + label = _("gain map metadata") +} + +GUI.optionwidgets.min_content_boost = dt.new_widget("slider") { + label = _('min content boost'), + tooltip = _( + 'How much darker an image can get, when shown on an HDR display, relative to the SDR rendition (linear, SDR = 1.0). Also called "GainMapMin". '), + hard_min = 0.9, + hard_max = 10, + soft_min = 0.9, + soft_max = 2, + step = 1, + digits = 1, + reset_callback = function(self) + self.value = 1.0 + end +} +GUI.optionwidgets.max_content_boost = dt.new_widget("slider") { + label = _('max content boost'), + tooltip = _( + 'How much brighter an image can get, when shown on an HDR display, relative to the SDR rendition (linear, SDR = 1.0). Also called "GainMapMax". \n\nMust not be lower than Min content boost'), + hard_min = 1, + hard_max = 10, + soft_min = 2, + soft_max = 10, + step = 1, + digits = 1, + reset_callback = function(self) + self.value = 6.0 + end +} +GUI.optionwidgets.hdr_capacity_min = dt.new_widget("slider") { + label = _('min HDR capacity'), + tooltip = _('Minimum display boost value for which the gain map is applied at all (linear, SDR = 1.0).'), + hard_min = 0.9, + hard_max = 10, + soft_min = 1, + soft_max = 2, + step = 1, + digits = 1, + reset_callback = function(self) + self.value = 1.0 + end +} +GUI.optionwidgets.hdr_capacity_max = dt.new_widget("slider") { + label = _('max HDR capacity'), + tooltip = _('Maximum display boost value for which the gain map is applied completely (linear, SDR = 1.0).'), + hard_min = 1, + hard_max = 10, + soft_min = 2, + soft_max = 10, + digits = 1, + step = 1, + reset_callback = function(self) + self.value = 6.0 + end +} + +GUI.optionwidgets.metadata_box = dt.new_widget("box") { + orientation = "vertical", + GUI.optionwidgets.metadata_label, + GUI.optionwidgets.min_content_boost, + GUI.optionwidgets.max_content_boost, + GUI.optionwidgets.hdr_capacity_min, + GUI.optionwidgets.hdr_capacity_max +} + +GUI.optionwidgets.encoding_variant_combo = dt.new_widget("combobox") { + label = _("each stack contains"), + tooltip = string.format(_([[Select the types of images in each stack. +This will determine the method used to generate UltraHDR. + +- %s: SDR image paired with a gain map image. +- %s: SDR image paired with an HDR image. +- %s: Each stack consists of a single SDR image. Gain maps will be copies of SDR images. +- %s: Each stack consists of a single HDR image. HDR will be tone mapped to SDR. + +By default, the first image in a stack is treated as SDR, and the second one is a gain map/HDR. +You can force the image into a specific stack slot by attaching "hdr" / "gainmap" tags to it. + +For HDR source images, apply a log2(203 nits/10000 nits) = -5.62 EV exposure correction +before generating UltraHDR.]]), _("SDR + gain map"), _("SDR + HDR"), _("SDR only"), _("HDR only")), + selected = 0, + changed_callback = function(self) + GUI.run.sensitive = self.selected and self.selected > 0 + if self.selected == ENCODING_VARIANT_SDR_AND_GAINMAP or self.selected == ENCODING_VARIANT_SDR_AUTO_GAINMAP then + GUI.optionwidgets.metadata_box.visible = true + GUI.optionwidgets.gainmap_downsampling_widget.visible = false + else + GUI.optionwidgets.metadata_box.visible = false + GUI.optionwidgets.gainmap_downsampling_widget.visible = true + end + end, + _("SDR + gain map"), -- ENCODING_VARIANT_SDR_AND_GAINMAP + _("SDR + HDR"), -- ENCODING_VARIANT_SDR_AND_HDR + _("SDR only"), -- ENCODING_VARIANT_SDR_AUTO_GAINMAP + _("HDR only") -- ENCODING_VARIANT_HDR_ONLY +} + +GUI.optionwidgets.selection_type_combo = dt.new_widget("combobox") { + label = _("selection contains"), + tooltip = string.format(_([[Select types of images selected in darktable. +This determines how the plugin groups images into separate stacks (each stack will produce a single UltraHDR image). + +- %s: All selected image(s) belong to one stack. There will be 1 output UltraHDR image. +- %s: Group images into stacks, using the source image path + filename (ignoring extension). + Use this method if the source images for a given stack are darktable duplicates. + +As an added precaution, each image in a stack needs to have the same resolution. +]]), _("one stack"), _("multiple stacks (use filename)")), + selected = 0, + _("one stack"), -- SELECTION_TYPE_ONE_STACK + _("multiple stacks (use filename)") -- SELECTION_TYPE_GROUP_BY_FNAME +} + +GUI.optionwidgets.quality_widget = dt.new_widget("slider") { + label = _('quality'), + tooltip = _('Quality of the output UltraHDR JPEG file'), + hard_min = 0, + hard_max = 100, + soft_min = 0, + soft_max = 100, + step = 1, + digits = 0, + reset_callback = function(self) + self.value = 95 + end +} + +GUI.optionwidgets.target_display_peak_nits_widget = dt.new_widget("slider") { + label = _('target display peak brightness (nits)'), + tooltip = _('Peak brightness of target display in nits (defaults to 10000)'), + hard_min = 203, + hard_max = 10000, + soft_min = 1000, + soft_max = 10000, + step = 10, + digits = 0, + reset_callback = function(self) + self.value = 10000 + end +} + +GUI.optionwidgets.gainmap_downsampling_widget = dt.new_widget("slider") { + label = _('gain map downsampling steps'), + tooltip = _( + 'Exponent (2^x) of the gain map downsampling factor.\nDownsampling reduces the gain map resolution.\n\n0 = don\'t downsample the gain map, 7 = maximum downsampling (128x)'), + hard_min = 0, + hard_max = 7, + soft_min = 0, + soft_max = 7, + step = 1, + digits = 0, + reset_callback = function(self) + self.value = 0 + end +} + +GUI.optionwidgets.encoding_settings_box = dt.new_widget("box") { + orientation = "vertical", + GUI.optionwidgets.selection_type_combo, + GUI.optionwidgets.encoding_variant_combo, + GUI.optionwidgets.quality_widget, + GUI.optionwidgets.gainmap_downsampling_widget, + GUI.optionwidgets.target_display_peak_nits_widget, + GUI.optionwidgets.metadata_box +} + +GUI.optionwidgets.executable_path_widget = df.executable_path_widget({"ultrahdr_app", "exiftool", "ffmpeg"}) +GUI.optionwidgets.executable_path_widget.visible = false + +GUI.optionwidgets.edit_executables_button = dt.new_widget("button") { + label = _("show / hide executables"), + tooltip = _("Show / hide settings for executable files required for the plugin functionality"), + clicked_callback = function() + GUI.optionwidgets.executable_path_widget.visible = not GUI.optionwidgets.executable_path_widget.visible + end +} + +GUI.options = dt.new_widget("box") { + orientation = "vertical", + GUI.optionwidgets.settings_label, + GUI.optionwidgets.encoding_settings_box, + GUI.optionwidgets.edit_executables_button, + GUI.optionwidgets.executable_path_widget, + GUI.optionwidgets.output_settings_box +} + +GUI.run = dt.new_widget("button") { + label = _("generate UltraHDR"), + tooltip = _("Generate UltraHDR image(s) from selection"), + clicked_callback = main +} + +load_preferences() + +local function install_module() + if flags.module_installed then + return + end + dt.register_lib( -- register module + namespace, -- Module name + _("UltraHDR"), -- name + true, -- expandable + true, -- resetable + { + [dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 99} + }, -- containers + dt.new_widget("box") { + orientation = "vertical", + GUI.options, + GUI.run + }, nil, -- view_enter + nil -- view_leave + ) +end + +local function destroy() + dt.gui.libs[namespace].visible = false +end + +local function restart() + dt.gui.libs[namespace].visible = true +end + +if dt.gui.current_view().id == "lighttable" then -- make sure we are in lighttable view + install_module() -- register the lib +else + if not flags.event_registered then -- if we are not in lighttable view then register an event to signal when we might be + -- https://www.darktable.org/lua-api/index.html#darktable_register_event + dt.register_event(namespace, "view-changed", -- we want to be informed when the view changes + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then -- if the view changes from darkroom to lighttable + install_module() -- register the lib + end + end) + flags.event_registered = true -- keep track of whether we have an event handler installed + end +end + +script_data.destroy = destroy +script_data.restart = restart +script_data.destroy_method = "hide" +script_data.show = restart + +return script_data diff --git a/contrib/video_ffmpeg.lua b/contrib/video_ffmpeg.lua new file mode 100644 index 00000000..f0713fa6 --- /dev/null +++ b/contrib/video_ffmpeg.lua @@ -0,0 +1,485 @@ +--[[Timelapse video plugin based on ffmpeg for darktable 2.4.X and 2.6.X + + copyright (c) 2018, 2019 Dominik Markiewicz + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +]] + +--[[About this plugin +This plugin will add the new export module "video ffmpeg". + +----REQUIRED SOFTWARE---- +ffmpeg + +----USAGE---- +1. Go to Lighttable +2. Select images you want to use as a video ffmpeg frames +3. In image export module select "video ffmpeg" +4. Configure you video settings +5. Export + +----WARNING---- +This script has been tested under Linux only +]] + +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 + +du.check_min_api_version("7.0.0", "video_ffmpeg") + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("video ffmpeg"), + purpose = _("timelapse video plugin based on ffmpeg"), + author = "Dominik Markiewicz", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contib/video_ffmpeg" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +local MODULE_NAME = "video_ffmpeg" + +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +---- DECLARATIONS + +local resolutions = { + ["QVGA"] = { + ["label"] = "QVGA 320x240 (4:3)", + ["w"] = 320, + ["h"] = 240 + }, + ["HVGA"] = { + ["label"] = "HVGA 480x320 (3:2)", + ["w"] = 480, + ["h"] = 320 + }, + ["VGA"] = { + ["label"] = "VGA 640x480 (4:3)", + ["w"] = 640, + ["h"] = 480 + }, + ["HDTV 720p"] = { + ["label"] = "HDTV 720p 1280x720 (16:9)", + ["w"] = 1280, + ["h"] = 720 + }, + ["HDTV 1080p"] = { + ["label"] = "HDTV 1080p 1920x1080 (16:9)", + ["w"] = 1920, + ["h"] = 1080 + }, + ["Cinema TV"] = { + ["label"] = "Cinema TV 2560x1080 (21:9)", + ["w"] = 2560, + ["h"] = 1080 + }, + ["2K"] = { + ["label"] = "2K 2048x1152 (16:9)", + ["w"] = 2048, + ["h"] = 1152 + }, + ["4K"] = { + ["label"] = "4K 4096x2304 (16:9)", + ["w"] = 4096, + ["h"] = 2304 + } +} + +local framerates = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "15", "16", "23.98", "24", "25", "29,97", "30", "48", "50", "59.94", "60", "120", "240", "300"} + +local formats = { + ["AVI"] = { + ["extension"] = "avi", + ["codecs"] = {"mpeg4", "h263p", "h264", "mpeg2video", "hevc", "vp9"} + }, + ["Matroska"] = { + ["extension"] = "mkv", + ["codecs"] = {"h264", "h263p", "mpeg4", "mpeg2video", "hevc", "vp9"} + }, + ["WebM"] = { + ["extension"] = "webm", + ["codecs"] = {"vp9", "h263p", "h264", "mpeg4", "mpeg2video", "hevc"} + }, + ["MP4"] = { + ["extension"] = "mp4", + ["codecs"] = {"h264", "h263p", "mpeg4", "mpeg2video"} + }, + ["QuickTime"] = { + ["extension"] = "mov", + ["codecs"] = {"h264", "h263p", "mpeg4", "mpeg2video"} + } +} + +local res_list = {} +for i, v in pairs(resolutions) do + table.insert(res_list, v["label"]) +end + +table.sort(res_list) + +-- inital fulfill list of all available formats +local format_list = {} +for i,v in pairs(formats) do + table.insert(format_list, i) +end +table.sort(format_list) + +-- initial fulfill list of all available codecs for the first (default if nothig saved yet) format +-- if format was changed and stored in preferences, this list will be replaced by one matching selected format +local codec_list = formats[format_list[1]]["codecs"] +table.sort(codec_list) + +local function extract_resolution(description) + for _, v in pairs(resolutions) do + if v["label"] == description then + return v["w"].."x"..v["h"] + end + end + return "100x100" +end + +---- GENERIC UTILS + +local function replace_combobox_elements(combobox, new_items, to_select) + if to_select == nil then + to_select = combobox.value + end + + to_select_idx = 1 + for i , name in ipairs(new_items) do + if name == to_select then + to_select_idx = i + break + end + end + + local old_elements_count = #combobox + for i, name in ipairs(new_items) do + combobox[i] = name + end + if old_elements_count > #new_items then + for j = old_elements_count, #new_items + 1, -1 do + combobox[j] = nil + end + end + + combobox.value = to_select_idx +end + +--[[ +This function allow to format string by substitute tokens like `{label}` by value provided in `symbols` table + ex: +```lua + format_string("I like {bananas} and {potatos}. {bananas}!", {["bananas"]: "darktable", ["potatos"]: "lua"}) + -> "I like darktable and lua. darktable!" +``` + +I you want to preserve given currly braces, you need to escape it by backslash like so: +```lua + format_string("it preserve \\{label\\} but substitute {label}", {["label"]: "this"}) + -> "It preserve {label} but substitute " +``` +--]] +local function format_string(label, symbols) + local es1, es2 = "\u{ffe0}", "\u{ffe1}" -- for simplicity, just some strange utf characters + local result = label:gsub("\\{", es1):gsub("\\}", es2) + for s,v in pairs(symbols) do + result = result:gsub("{"..s.."}", v) + end + return result:gsub(es1, "{"):gsub(es2, "}") +end + +----- COMPONENTS + +local function combobox_pref_read(name, all_values) + local value = dt.preferences.read(MODULE_NAME, name, "string") + for i,v in pairs(all_values) do + if v == value then return i end + end + return 1 +end + +local function combobox_pref_write(name) + local writer = function(widget) + dt.preferences.write(MODULE_NAME, name, "string", widget.value) + end + return writer +end + +local function string_pref_read(name, default) + local value = dt.preferences.read(MODULE_NAME, name, "string") + if value ~= nil and value ~= "" then return value end + return default +end + +local function string_pref_write(name, widget_attribute) + widget_attribute = widget_attribute or "value" + local writer = function(widget) + dt.preferences.write(MODULE_NAME, name, "string", widget[widget_attribute]) + end + return writer +end + +local framerates_selector = dt.new_widget("combobox"){ + label = _("frame rate"), + tooltip = _("select frame rate of output video"), + value = combobox_pref_read("framerate", framerates), + changed_callback = combobox_pref_write("framerate"), + table.unpack(framerates) +} + +local res_selector = dt.new_widget("combobox"){ + label = _("resolution"), + tooltip = _("select resolution of output video"), + value = combobox_pref_read("resolution", res_list), + changed_callback = combobox_pref_write("resolution"), + table.unpack(res_list) +} + +local codec_selector = dt.new_widget("combobox"){ + label = _("codec"), + tooltip = _("select codec"), + value = combobox_pref_read("codec", codec_list), + changed_callback = combobox_pref_write("codec"), + table.unpack(codec_list) +} + +local format_selector = dt.new_widget("combobox"){ + label = _("format container"), + tooltip = _("select format of output video"), + value = combobox_pref_read("format", format_list), + changed_callback = function(widget) + combobox_pref_write("format")(widget) + codec_list = formats[widget.value]["codecs"] + table.sort(codec_list) + replace_combobox_elements(codec_selector, codec_list) + end, + table.unpack(format_list) +} + +local destination_label = dt.new_widget("section_label"){ + label = _("output file destination"), + tooltip = _("settings of output file destination and name") +} + +local defaultVideoDir = '' +if dt.configuration.running_os == "windows" then + defaultVideoDir = os.getenv("USERPROFILE")..PS .."videos" +elseif dt.configuration.running_os == "macos" then + defaultVideoDir = os.getenv("HOME")..PS.."Videos" +else + local handle = io.popen("xdg-user-dir VIDEOS") + defaultVideoDir = handle:read() + handle:close() +end + +local output_directory_chooser = dt.new_widget("file_chooser_button"){ + 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), + changed_callback = string_pref_write("export_path") +} + +local auto_output_directory_btn = dt.new_widget("check_button") { + label = "", + tooltip = _("if selected, output video will be placed in the same directory as first of selected images"), + value = not dt.preferences.read(MODULE_NAME, "not_auto_output_directory", "bool"), -- reverse, for true as default + clicked_callback = function (widget) + dt.preferences.write(MODULE_NAME, "not_auto_output_directory", "bool", not widget.value) + output_directory_chooser.sensitive = not output_directory_chooser.sensitive + end +} + +local destination_box = dt.new_widget("box") { + orientation = "horizontal", + auto_output_directory_btn, + output_directory_chooser +} + +local override_output_cb = dt.new_widget("check_button"){ + label = _("override output file on conflict"), + tooltip = _("if checked, in case of file name conflict, the file will be overwritten"), + value = dt.preferences.read(MODULE_NAME, "override_output", "bool"), + clicked_callback = function (widget) + dt.preferences.write(MODULE_NAME, "override_output", "bool", widget.value) + end +} + +local filename_entry = dt.new_widget("entry"){ + tooltip = _("enter output file name without extension.\n\n".. + "You can use some placeholders:\n".. + "- {time} - time in format HH-mm-ss\n".. + "- {date} - date in foramt YYYY-mm-dd\n".. + "- {first_file} - name of first input file\n".. + "- {last_file} - name of last last_file" + ), + text = string_pref_read("filename_entry","timelapse_{date}_{time}"), + -- changed_callback = string_pref_write("filename_entry", "text") -- Not imlemented yet +} + +local output_box = dt.new_widget("box"){ + orientation="vertical", + destination_label, + override_output_cb, + destination_box, + filename_entry, +} + +local open_after_export_cb = dt.new_widget("check_button"){ + label = _(" open after export"), + tooltip = _("open video file after successful export"), + value = dt.preferences.read(MODULE_NAME, "open_after_export", "bool"), + clicked_callback = function (widget) + dt.preferences.write(MODULE_NAME, "open_after_export", "bool", widget.value) + end +} + +local widgets_list = {} +if not df.check_if_bin_exists("ffmpeg") then + table.insert(widgets_list, df.executable_path_widget({"ffmpeg"})) +end +table.insert(widgets_list, res_selector) +table.insert(widgets_list, framerates_selector) +table.insert(widgets_list, format_selector) +table.insert(widgets_list, codec_selector) +table.insert(widgets_list, output_box) +table.insert(widgets_list, open_after_export_cb) + + +local module_widget = dt.new_widget("box") { + orientation = "vertical", + table.unpack(widgets_list) +} + +---- 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)) +end + +local function init_export(storage, img_format, images, high_quality, extra_data) + -- store filename preference here cause there is no changed_callback on entry yet + string_pref_write("filename_entry", "text")(filename_entry) + + extra_data["images"] = images -- needed, to preserve images order + extra_data["tmp_dir"] = dt.configuration.tmp_dir..PS..MODULE_NAME.."_"..os.time() + extra_data["fps"] = framerates_selector.value + extra_data["res"] = extract_resolution(res_selector.value) + extra_data["codec"] = codec_selector.value + extra_data["img_ext"] = "."..img_format.extension + local override_output = override_output_cb.value + local output_directory = auto_output_directory_btn.value and images[1].path or output_directory_chooser.value + local filename_mappings = { + date = os.date("%Y-%m-%d"), + time = os.date("%H-%M-%S"), + first_file = images[1].filename, + last_file = images[#images].filename + } + local output_extension = "."..formats[format_selector.value]["extension"] + local filename = format_string(filename_entry.text, filename_mappings) + local path = output_directory..PS..filename..output_extension + if not override_output then + path = df.create_unique_filename(path) + end + + extra_data["output_file"] = path + extra_data["open_after_export"] = open_after_export_cb.value +end + +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")) + return + end + local dir = extra_data["tmp_dir"] + local fps = extra_data["fps"] + local res = extra_data["res"] + local codec = extra_data["codec"] + local img_ext = extra_data["img_ext"] + local output_file = extra_data["output_file"] + + local dir_create_result = df.mkdir(df.sanitize_filename(df.get_path(output_file))) + if dir_create_result ~= 0 then return dir_create_result end + + local cmd = ffmpeg_path.." -y -r "..fps.." -i "..dir..PS.."%d"..img_ext.." -s:v "..res.." -c:v "..codec.." -crf 18 -preset veryslow "..df.sanitize_filename(output_file) + return dsys.external_command(cmd), output_file +end + +local function finalize_export(storage, images_table, extra_data) + local tmp_dir = extra_data["tmp_dir"] + + dt.print(_("prepare merge process")) + + local result = df.mkdir(df.sanitize_filename(tmp_dir)) + if result ~= 0 then dt.print(_("ERROR: cannot create temp directory")) end + + local images = extra_data["images"] + -- rename all images to consecutive numbers + for i, file in pairs(images) do + local filename = images_table[file] + dt.print_error(filename, file.filename) + df.file_move(filename, tmp_dir .. PS .. i .. extra_data["img_ext"]) + end + 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")) + else + dt.print(_("SUCCESS")) + if extra_data["open_after_export"] then + dsys.launch_default_app(df.sanitize_filename(path)) + end + end + + df.rmdir(df.sanitize_filename(tmp_dir)) +end + +-- script_manager integration + +local function destroy() + dt.destroy_storage("module_video_ffmpeg") +end + +dt.register_storage( + "module_video_ffmpeg", + _("video ffmpeg"), + show_status, + finalize_export, + nil, + init_export, + module_widget +) + +-- script_manager integration + +script_data.destroy = destroy + +return script_data diff --git a/contrib/video_mencoder.lua b/contrib/video_mencoder.lua deleted file mode 100644 index 3f395872..00000000 --- a/contrib/video_mencoder.lua +++ /dev/null @@ -1,119 +0,0 @@ ---[[ - This file is part of darktable, - Copyright 2014 by Tobias Jakobs. - - 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 . -]] ---[[ -darktable video export script - -ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT -* mencoder (MEncoder is from the MPlayer Team) -* xdg-open -* xdg-user-dir - -WARNING -This script is only tested with Linux - -USAGE -* require this script from your main lua file -]] - -local dt = require "darktable" -local df = require "lib/dtutils.file" -require "official/yield" -local gettext = dt.gettext - -dt.configuration.check_version(...,{2,0,1},{3,0,0},{4,0,0},{5,0,0}) - --- Tell gettext where to find the .mo file translating messages for a particular domain -gettext.bindtextdomain("video_mencoder",dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("video_mencoder", msgid) -end - -local function show_status(storage, image, format, filename, number, total, high_quality, extra_data) - dt.print("Export Image "..tostring(number).."/"..tostring(total)) -end - -local function create_video_mencoder(storage, image_table, extra_data) - if not df.check_if_bin_exists("mencoder") then - dt.print_error(_("mencoder not found")) - return - end - if not df.check_if_bin_exists("xdg-open") then - 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")) - return - end - - exportDirectory = dt.preferences.read("video_mencoder","ExportDirectory","string") - exportFilename = "output.avi" - framsePerSecond = dt.preferences.read("video_mencoder","FramesPerSecond","integer") - - dt.print_error("Will try to create video now") - -- Set the codec - local codec = "" - local codecPreferences = "" - - codecPreferences = dt.preferences.read("video_mencoder","Codec","string") - if (codecPreferences == "H.264 encoding") then - codec = 'x264' - end - if (codecPreferences == "XviD encoding") then - codec = 'xvid' - end - if not codecPreferences then - codec = 'x264' - end - - -- Create the command - local command = "mencoder -idx -nosound -noskip -ovc "..codec.." -lavcopts vcodec=mjpeg -o "..exportDirectory.."/"..exportFilename.." -mf fps="..framsePerSecond .." mf://" - - for _,v in pairs(image_table) do - command = command..v.."," - end - - dt.print_error("this is the command: "..command) - dt.control.execute( command) - - dt.print("Video created in "..exportDirectory) - - if ( dt.preferences.read("video_mencoder","OpenVideo","bool") == true ) then - local playVideoCommand = "xdg-open "..exportDirectory.."/"..exportFilename - dt.control.execute( playVideoCommand) - end -end - --- Preferences -dt.preferences.register("video_mencoder", "FramesPerSecond", "float", "Video exort (MEncoder): Frames per second", "Frames per Second in the Video export", 15, 1, 99, 0.1 ) -dt.preferences.register("video_mencoder", "OpenVideo", "bool", "Video exort (MEncoder): Open video after export", "Opens the Video after the export with the standard video player", false ) - -local handle = io.popen("xdg-user-dir VIDEOS") -local result = handle:read() -handle:close() -if (result == nil) then - result = "" -end -dt.preferences.register("video_mencoder", "ExportDirectory", "directory", "Video exort (MEncoder): Video export directory","A directory that will be used to export a Video",result) - --- Get the MEncoder codec list with: mencoder -ovc help -dt.preferences.register("video_mencoder", "Codec", "enum", "Video exort (MEncoder): Codec","Video codec","H.264 encoding","H.264 encoding","XviD encoding") - --- Register -dt.register_storage("video_mencoder", "Video Export (MEncoder)", show_status, create_video) diff --git a/data/icons/blank20.png b/data/icons/blank20.png new file mode 100644 index 00000000..81ed5157 Binary files /dev/null and b/data/icons/blank20.png differ diff --git a/data/icons/path20.png b/data/icons/path20.png new file mode 100644 index 00000000..1021d864 Binary files /dev/null and b/data/icons/path20.png differ diff --git a/examples/api_version.lua b/examples/api_version.lua index 971f14c0..bf8a1c17 100644 --- a/examples/api_version.lua +++ b/examples/api_version.lua @@ -23,5 +23,36 @@ 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() + -- nothing to destroy +end + local result = dt.configuration.api_version_string -dt.print_error("API Version: "..result) +dt.print_log("API Version: " .. result) +dt.print("API " .. _("version") .. ": " .. result) + +-- set the destroy routine so that script_manager can call it when +-- it's time to destroy the script and then return the data to +-- script_manager +local script_data = {} + +script_data.metadata = { + name = _("APIversion"), + purpose = _("display api_version example"), + author = "Tobias Jakobs", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/api_version" +} + +script_data.destroy = destroy + +return script_data diff --git a/examples/darkroom_demo.lua b/examples/darkroom_demo.lua new file mode 100644 index 00000000..b76b552e --- /dev/null +++ b/examples/darkroom_demo.lua @@ -0,0 +1,128 @@ +--[[ + This file is part of darktable, + copyright (c) 2019 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 . +]] +--[[ + darkroom_demo - an example script demonstrating how to control image display in darkroom mode + + darkroom_demo is an example script showing how to control the currently displayed image in + darkroom mode using lua. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + * none + + USAGE + * require this script from your main lua file + + BUGS, COMMENTS, SUGGESTIONS + * Send to Bill Ferguson, wpferguson@gmail.com + + CHANGES +]] + +local dt = require "darktable" +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 + +-- script_manager integration to allow a script to be removed +-- without restarting darktable +local function destroy() + -- nothing to destroy +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE_NAME = "darkroom_demo" +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- T R A N S L A T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N +-- - - - - - - - - - - - - - - - - - - - - - - - + +-- alias dt.control.sleep to sleep +local sleep = dt.control.sleep + +-- save the configuration + +local current_view = dt.gui.current_view() + +-- check that there is an image selected, otherwise we can't activate darkroom viewe + +local images = dt.gui.action_images +dt.print_log(#images .. " images selected") +if not images or #images == 0 then + dt.print_log("no images selected, creating selection") + dt.print_log("using image " .. dt.collection[1].filename) + dt.gui.selection({dt.collection[1]}) +end + +-- enter darkroom view + +dt.gui.current_view(dt.gui.views.darkroom) + +local max_images = 10 + +dt.print(_("showing images, with a pause between each")) +sleep(1500) + +-- display first 10 images of collection pausing for a second between each + +for i, img in ipairs(dt.collection) do + dt.print(string.format(_("displaying image "), i)) + dt.gui.views.darkroom.display_image(img) + sleep(1500) + if i == max_images then + break + end +end + +-- return to lighttable view + +dt.print(_("restoring view")) +sleep(1500) +dt.gui.current_view(current_view) + +-- 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 = _("darkroom demo"), + purpose = _("example demonstrating how to control image display in darkroom mode"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/darkroom_demo" +} + +script_data.destroy = destroy + +return script_data diff --git a/examples/gettextExample.lua b/examples/gettextExample.lua index 856a8252..42e1a479 100644 --- a/examples/gettextExample.lua +++ b/examples/gettextExample.lua @@ -53,24 +53,45 @@ LUA ERROR Hallo Welt! ]] local dt = require "darktable" -dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0})) +local du = require "lib/dtutils" + +--check API version +du.check_min_api_version("3.0.0", "gettextExample") + +-- script_manager integration to allow a script to be removed +-- without restarting darktable +local function destroy() + -- nothing to destroy +end -- Not translated Text dt.print_error("Hello World!") -local gettext = dt.gettext --- Translate a string using the darktable textdomain -dt.print_error(gettext.gettext("image")) - --- Tell gettext where to find the .mo file translating messages for a particular domain +local gettext = dt.gettext.gettext -gettext.bindtextdomain("gettextExample",dt.configuration.config_dir.."/lua/locale/") --- Translate a string using the specified textdomain -dt.print_error(gettext.dgettext("gettextExample", 'Hello World!')) +-- Translate a string using the darktable textdomain +dt.print_error(gettext("image")) -- Define a local function called _ to make the code more readable and have it call dgettext -- with the proper domain. local function _(msgid) - return gettext.dgettext("gettextExample", msgid) + return gettext(msgid) end -dt.print_error(_('Hello World!')) + +dt.print_error(_("hello world!")) + +-- set the destroy routine so that script_manager can call it when +-- it's time to destroy the script and then return the data to +-- script_manager +local script_data = {} + +script_data.metadata = { + name = _("gettext example"), + purpose = _("example of how translations works"), + author = "Tobias Jakobs", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/gettextExample" +} + +script_data.destroy = destroy + +return script_data diff --git a/examples/gui_action.lua b/examples/gui_action.lua new file mode 100644 index 00000000..9fee3619 --- /dev/null +++ b/examples/gui_action.lua @@ -0,0 +1,142 @@ +local dt = require "darktable" + +local NaN = 0/0 + +local wg = {} + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("gui action"), + purpose = _("example of how to use darktable.gui.action() calls"), + author = "Diederik ter Rahe", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/gui_action" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +wg.action = dt.new_widget("entry"){ + text = "lib/filter/view", + placeholder = _("action path"), + tooltip = _("enter the full path of an action, for example 'lib/filter/view'") + } + +wg.instance = dt.new_widget("combobox"){ + label = _("instance"), + tooltip = _("the instance of an image processing module to execute action on"), + "0", "+1", "-1", "+2", "-2", "+3", "-3", "+4", "-4", "+5", "-5", "+6", "-6", "+7", "-7", "+8", "-8", "+9", "-9" +} + +wg.element = dt.new_widget("entry"){ + text = "", + placeholder = _("action element"), + tooltip = _("enter the element of an action, for example 'selection', or leave empty for default") +} + +wg.effect = dt.new_widget("entry"){ + text = "next", + placeholder = _("action effect"), + tooltip = _("enter the effect of an action, for example 'next', or leave empty for default") +} + +wg.speed = dt.new_widget("entry"){ + text = "1", + placeholder = _("action speed"), + tooltip = _("enter the speed to use in action execution, or leave empty to only read state") +} + +wg.check = dt.new_widget("check_button"){ + label = _('perform action'), + tooltip = _('perform action or only read return'), + clicked_callback = function() + wg.speed.sensitive = wg.check.value + end, + value = true +} + +wg.return_value = dt.new_widget("entry"){ + text = "", + sensitive = false +} + +dt.register_lib( + "execute_action", -- Module name + _("execute gui actions"), -- name + true, -- expandable + false, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 100}, + [dt.gui.views.darkroom] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 100}}, + dt.new_widget("box") + { + orientation = "vertical", + + dt.new_widget("box") + { + orientation = "horizontal", + dt.new_widget("label"){label = _("action path"), halign = "start"}, + wg.action + }, + wg.instance, + dt.new_widget("box") + { + orientation = "horizontal", + dt.new_widget("label"){label = _("element"), halign = "start"}, + wg.element + }, + dt.new_widget("box") + { + orientation = "horizontal", + dt.new_widget("label"){label = _("effect"), halign = "start"}, + wg.effect + }, + wg.check, + dt.new_widget("box") + { + orientation = "horizontal", + dt.new_widget("label"){label = _("speed"), halign = "start"}, + wg.speed + }, + dt.new_widget("button") + { + label = _("execute action"), + tooltip = _("execute the action specified in the fields above"), + clicked_callback = function(_) + local sp = NaN + if wg.check.value then sp = wg.speed.text end + wg.return_value.text = dt.gui.action(wg.action.text, tonumber(wg.instance.value), wg.element.text, wg.effect.text, tonumber(sp)) + end + }, + dt.new_widget("box") + { + orientation = "horizontal", + dt.new_widget("label"){label = "return value:", halign = "start"}, + wg.return_value + }, + } + ) + +local function restart() + dt.gui.libs["execute_action"].visible = true +end + +local function destroy() + dt.gui.libs["execute_action"].visible = false +end + +script_data.destroy = destroy +script_data.destroy_method = "hide" +script_data.restart = restart +script_data.show = restart + +return script_data diff --git a/examples/hello_world.lua b/examples/hello_world.lua index 6a19880e..6e04efc4 100644 --- a/examples/hello_world.lua +++ b/examples/hello_world.lua @@ -27,9 +27,40 @@ USAGE ]] local dt = require "darktable" -dt.configuration.check_version(...,{2,0,0},{3,0,0},{4,0,0},{5,0,0}) +local du = require "lib/dtutils" -dt.print("hello, world") +-- 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")) + +-- 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 -- -- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua diff --git a/examples/lighttable_demo.lua b/examples/lighttable_demo.lua new file mode 100644 index 00000000..e28454cd --- /dev/null +++ b/examples/lighttable_demo.lua @@ -0,0 +1,229 @@ +--[[ + This file is part of darktable, + copyright (c) 2019 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 . +]] +--[[ + lighttable_demo - an example script demonstrating how to control lighttable display modes + + lighttable_demo is an example script showing how to control lighttable layout, sorting, and + filtering from a lua script. If the selected directory has different ratings, color labels, etc, + then the sorting and filtering display is a little clearer. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + * none + + USAGE + * require this script from your main lua file + + BUGS, COMMENTS, SUGGESTIONS + * Send to Bill Ferguson, wpferguson@gmail.com + + CHANGES +]] + +local dt = require "darktable" +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", "lighttable_demo") -- darktable 3.0 + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE_NAME = "lighttable" +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +-- script_manager integration to allow a script to be removed +-- without restarting darktable +local function destroy() + -- nothing to destroy +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 function _(msgid) + return gettext(msgid) +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N +-- - - - - - - - - - - - - - - - - - - - - - - - + +-- alias dt.control.sleep to sleep +local sleep = dt.control.sleep + +local layouts = { + "DT_LIGHTTABLE_LAYOUT_ZOOMABLE", + "DT_LIGHTTABLE_LAYOUT_FILEMANAGER", + "DT_LIGHTTABLE_LAYOUT_CULLING", + "DT_LIGHTTABLE_LAYOUT_CULLING_DYNAMIC", +} + +local sorts = { + "DT_COLLECTION_SORT_NONE", + "DT_COLLECTION_SORT_FILENAME", + "DT_COLLECTION_SORT_DATETIME", + "DT_COLLECTION_SORT_RATING", + "DT_COLLECTION_SORT_ID", + "DT_COLLECTION_SORT_COLOR", + "DT_COLLECTION_SORT_GROUP", + "DT_COLLECTION_SORT_PATH", + "DT_COLLECTION_SORT_CUSTOM_ORDER", + "DT_COLLECTION_SORT_TITLE", + "DT_COLLECTION_SORT_DESCRIPTION", + "DT_COLLECTION_SORT_ASPECT_RATIO", + "DT_COLLECTION_SORT_SHUFFLE" +} + +local sort_orders = { + "DT_COLLECTION_SORT_ORDER_ASCENDING", + "DT_COLLECTION_SORT_ORDER_DESCENDING" +} + +local ratings = { + "DT_COLLECTION_FILTER_ALL", + "DT_COLLECTION_FILTER_STAR_NO", + "DT_COLLECTION_FILTER_STAR_1", + "DT_COLLECTION_FILTER_STAR_2", + "DT_COLLECTION_FILTER_STAR_3", + "DT_COLLECTION_FILTER_STAR_4", + "DT_COLLECTION_FILTER_STAR_5", + "DT_COLLECTION_FILTER_REJECT", + "DT_COLLECTION_FILTER_NOT_REJECT" +} + +local rating_comparators = { + "DT_COLLECTION_RATING_COMP_LT", + "DT_COLLECTION_RATING_COMP_LEQ", + "DT_COLLECTION_RATING_COMP_EQ", + "DT_COLLECTION_RATING_COMP_GEQ", + "DT_COLLECTION_RATING_COMP_GT", + "DT_COLLECTION_RATING_COMP_NE" +} + +local zoom_levels = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + + +-- save filter, view, and collection parameters + +local current_layout = dt.gui.libs.lighttable_mode.layout() +local current_zoom_level = dt.gui.libs.lighttable_mode.zoom_level() +local current_rating = dt.gui.libs.filter.rating() +local current_rating_comparator = dt.gui.libs.filter.rating_comparator() +local current_sort = dt.gui.libs.filter.sort() +local current_sort_order = dt.gui.libs.filter.sort_order() + +-- cycle through layouts and zooms + +dt.print(_("lighttable layout and zoom level demonstration")) +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) + 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)) + 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)) + sleep(1500) + end +end + +dt.print_log("finished layout and zoom level testing") +dt.print_log("starting sort demonstration") +-- cycle through sorts + +dt.print(_("lighttable sorting demonstration")) +dt.print_log("setting lighttable to filemanager mode") +dt.gui.libs.lighttable_mode.layout("DT_LIGHTTABLE_LAYOUT_FILEMANAGER") +sleep(500) +dt.print_log("setting lighttable to zoom level 5") +dt.gui.libs.lighttable_mode.zoom_level(5) +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)) + 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)) + sleep(1500) + end +end + +-- cycle through filters + +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)) + 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)) + sleep(1500) + end +end + +-- restore settings + +dt.print(_("restoring settings")) + +current_layout = dt.gui.libs.lighttable_mode.layout(current_layout) +current_zoom_level = dt.gui.libs.lighttable_mode.zoom_level(current_zoom_level) +current_rating = dt.gui.libs.filter.rating(current_rating) +current_rating_comparator = dt.gui.libs.filter.rating_comparator(current_rating_comparator) +current_sort =dt.gui.libs.filter.sort(current_sort) +current_sort_order = dt.gui.libs.filter.sort_order(current_sort_order) + +-- 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 = _("lighttable demo"), + purpose = _("example demonstrating how to control lighttable display modes"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/lighttable_demo" +} + +script_data.destroy = destroy + +return script_data diff --git a/examples/moduleExample.lua b/examples/moduleExample.lua index eda87481..89c56bc2 100644 --- a/examples/moduleExample.lua +++ b/examples/moduleExample.lua @@ -18,9 +18,9 @@ --[[ USAGE -* require this script from your main lua file +* require this script from your luarc file To do this add this line to the file .config/darktable/luarc: -require "moduleExample" +require "examples/moduleExample" * it creates a new example lighttable module @@ -31,38 +31,120 @@ https://www.darktable.org/lua-api/index.html.php#darktable_new_widget ]] local dt = require "darktable" +local du = require "lib/dtutils" -dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0}) +du.check_min_api_version("7.0.0", "moduleExample") --- add a new lib -local check_button = dt.new_widget("check_button"){label = "MyCheck_button", value = true} -local combobox = dt.new_widget("combobox"){label = "MyCombobox", value = 2, "8", "16", "32"} +-- https://www.darktable.org/lua-api/index.html#darktable_gettext +local gettext = dt.gettext.gettext ---https://www.darktable.org/lua-api/ar01s02s54.html.php +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = ("module example"), + purpose = _("example of how to create a lighttable module"), + author = "Tobias Jakobs", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/moduleExample" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +-- translation + +-- declare a local namespace and a couple of variables we'll need to install the module +local mE = {} +mE.widgets = {} +mE.event_registered = false -- keep track of whether we've added an event callback or not +mE.module_installed = false -- keep track of whether the module is module_installed + +--[[ We have to create the module in one of two ways depending on which view darktable starts + in. In orker to not repeat code, we wrap the darktable.register_lib in a local function. + ]] + +local function install_module() + if not mE.module_installed then + -- https://www.darktable.org/lua-api/index.html#darktable_register_lib + dt.register_lib( + "exampleModule", -- Module name + _("example module"), -- name + true, -- expandable + false, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers + -- https://www.darktable.org/lua-api/types_lua_box.html + dt.new_widget("box") -- widget + { + orientation = "vertical", + dt.new_widget("button") + { + label = _("my ") .. "button", + clicked_callback = function (_) + dt.print(_("button clicked")) + end + }, + table.unpack(mE.widgets), + }, + nil,-- view_enter + nil -- view_leave + ) + mE.module_installed = true + end +end + +-- script_manager integration to allow a script to be removed +-- without restarting darktable +local function destroy() + dt.gui.libs["exampleModule"].visible = false -- we haven't figured out how to destroy it yet, so we hide it for now +end + +local function restart() + dt.gui.libs["exampleModule"].visible = true -- the user wants to use it again, so we just make it visible and it shows up in the UI +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} + +-- https://www.darktable.org/lua-api/types_lua_combobox.html +local combobox = dt.new_widget("combobox"){label = _("my ") .. "combobox", value = 2, "8", "16", "32"} + +-- https://www.darktable.org/lua-api/types_lua_entry.html local entry = dt.new_widget("entry") { text = "test", - placeholder = "placeholder", + placeholder = _("placeholder"), is_password = false, editable = true, - tooltip = "Tooltip Text", + tooltip = _("tooltip text"), reset_callback = function(self) self.text = "text" end } +-- https://www.darktable.org/lua-api/types_lua_file_chooser_button.html local file_chooser_button = dt.new_widget("file_chooser_button") { - title = "MyFile_chooser_button", -- The title of the window when choosing a file + title = _("my ") .. "file_chooser_button", -- The title of the window when choosing a file value = "", -- The currently selected file is_directory = false -- True if the file chooser button only allows directories to be selecte } +-- https://www.darktable.org/lua-api/types_lua_label.html local label = dt.new_widget("label") -label.label = "MyLabel" -- This is an alternative way to the "{}" syntax to set a property +label.label = _("my ") .. "label" -- This is an alternative way to the "{}" syntax to set a property +-- https://www.darktable.org/lua-api/types_lua_separator.html local separator = dt.new_widget("separator"){} + +-- https://www.darktable.org/lua-api/types_lua_slider.html local slider = dt.new_widget("slider") { - label = "MySlider", + label = _("my ") .. "slider", soft_min = 10, -- The soft minimum value for the slider, the slider can't go beyond this point soft_max = 100, -- The soft maximum value for the slider, the slider can't go beyond this point hard_min = 0, -- The hard minimum value for the slider, the user can't manually enter a value beyond this point @@ -70,69 +152,44 @@ local slider = dt.new_widget("slider") value = 52 -- The current value of the slider } -if (dt.configuration.api_version_major >= 6) then - local section_label = dt.new_widget("section_label") - { - label = "MySectionLabel" - } - - dt.register_lib( - "exampleModule", -- Module name - "exampleModule", -- name - true, -- expandable - false, -- resetable - {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers - dt.new_widget("box") -- widget - { - orientation = "vertical", - dt.new_widget("button") - { - label = "MyButton", - clicked_callback = function (_) - dt.print("Button clicked") - end - }, - check_button, - combobox, - entry, - file_chooser_button, - label, - separator, - slider, - section_label - }, - nil,-- view_enter - nil -- view_leave - ) +-- pack the widgets in a table for loading in the module + +table.insert(mE.widgets, check_button) +table.insert(mE.widgets, combobox) +table.insert(mE.widgets, entry) +table.insert(mE.widgets, file_chooser_button) +table.insert(mE.widgets, label) +table.insert(mE.widgets, separator) +table.insert(mE.widgets, slider) + +-- ... and tell dt about it all + + +if dt.gui.current_view().id == "lighttable" then -- make sure we are in lighttable view + install_module() -- register the lib else - dt.register_lib( - "exampleModule", -- Module name - "exampleModule", -- name - true, -- expandable - false, -- resetable - {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers - dt.new_widget("box") -- widget - { - orientation = "vertical", - dt.new_widget("button") - { - label = "MyButton", - clicked_callback = function (_) - dt.print("Button clicked") - end - }, - check_button, - combobox, - entry, - file_chooser_button, - label, - separator, - slider - }, - nil,-- view_enter - nil -- view_leave - ) + if not mE.event_registered then -- if we are not in lighttable view then register an event to signal when we might be + -- https://www.darktable.org/lua-api/index.html#darktable_register_event + dt.register_event( + "mdouleExample", "view-changed", -- we want to be informed when the view changes + function(event, old_view, new_view) + if new_view.name == "lighttable" and old_view.name == "darkroom" then -- if the view changes from darkroom to lighttable + install_module() -- register the lib + end + end + ) + mE.event_registered = true -- keep track of whether we have an event handler installed + end end +-- 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 +script_data.destroy = destroy +script_data.restart = restart -- only required for lib modules until we figure out how to destroy them +script_data.destroy_method = "hide" -- tell script_manager that we are hiding the lib so it knows to use the restart function +script_data.show = restart -- if the script was "off" when darktable exited, the module is hidden, so force it to show on start + +return script_data -- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua -- kate: hl Lua; diff --git a/examples/multi_os.lua b/examples/multi_os.lua new file mode 100644 index 00000000..21dc0cb1 --- /dev/null +++ b/examples/multi_os.lua @@ -0,0 +1,271 @@ +--[[ + This file is part of darktable, + copyright (c) 2017,2018 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 . +]] +--[[ + multi_os - an example script that runs on linux, MacOS, and Windows. + + multi_os is an example of how to write a script that will run on different + operating systems. It uses the lua-scripts libraries to lessen the amount + of code that needs to be written, as well as gaining access to tested + cross-platform routines. This script also performs a function that some + may find useful. It creates a button in the lighttable selected images module + that extracts the embedded jpeg image from a raw file, then imports it and groups + it with the raw file. A keyboard shortcut is also created. A key combination can + be assigned to the shortcut in the lua preferences and then the action can be invoked + by hovering over the image and pressing the key combination. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + * ufraw-batch - https://ufraw.sourceforge.net + MacOS - install with homebrew + + USAGE + * require this script from your main lua file + * start darktable, open prefreences, go to lua options + and update the executable location if you are running + Windows or MacOS, then restart darktable. + * select an image or images + * click the button to extract the jpeg + + BUGS, COMMENTS, SUGGESTIONS + * Send to Bill Ferguson, wpferguson@gmail.com + + CHANGES +]] + +--[[ + require "darktable" provides the interface to the darktable lua functions used to + interact with darktable +]] + +local dt = require "darktable" + +--[[ + require "lib/..." provides access to functions that have been pulled from various + scripts and consolidated into libraries. Using the libraries eliminates having to + recode common functions in every script. +]] + +local du = require "lib/dtutils" -- utilities +local df = require "lib/dtutils.file" -- file utilities +local dtsys = require "lib/dtutils.system" -- system utilities + +--[[ + darktable is an international program, and it's user interface has been translated into + many languages. The lua API provides gettext which is a function that looks for and replaces + strings with the translated equivalents based on locale. Even if you don't provide the + translations, inserting this lays the groundwork for anyone who wants to translate the strings. +]] + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +--[[ + Check that the current api version is greater than or equal to the specified minimum. If it's not + then du.check_min_api_version will print an error to the log and return false. If the minimum api is + not met, then just refuse to load and return. Optionally, you could print an error message to the + screen stating that you couldn't load because the minimum api version wasn't met. +]] + +du.check_min_api_version("7.0.0", "multi_os") + +--[[ + copy_image_attributes is a local subroutine to copy image attributes in the database from the raw image + to the extracted jpeg image. Even though the subroutine could act on the variables in the main program + directly, passing them as arguments and using them in the subroutine is much safer. +]] + +local function copy_image_attributes(from, to, ...) + local args = {...} + if #args == 0 then + args[1] = "all" + end + if args[1] == "all" then + args[1] = "rating" + args[2] = "colors" + args[3] = "exif" + args[4] = "meta" + args[5] = "GPS" + end + for _,arg in ipairs(args) do + if arg == "rating" then + to.rating = from.rating + elseif arg == "colors" then + to.red = from.red + to.blue = from.blue + to.green = from.green + to.yellow = from.yellow + to.purple = from.purple + elseif arg == "exif" then + to.exif_maker = from.exif_maker + to.exif_model = from.exif_model + to.exif_lens = from.exif_lens + to.exif_aperture = from.exif_aperture + to.exif_exposure = from.exif_exposure + to.exif_focal_length = from.exif_focal_length + to.exif_iso = from.exif_iso + to.exif_datetime_taken = from.exif_datetime_taken + to.exif_focus_distance = from.exif_focus_distance + to.exif_crop = from.exif_crop + elseif arg == "GPS" then + to.elevation = from.elevation + to.longitude = from.longitude + to.latitude = from.latitude + elseif arg == "meta" then + to.publisher = from.publisher + to.title = from.title + to.creator = from.creator + to.rights = from.rights + to.description = from.description + else + dt.print_error("Unrecognized option to copy_image_attributes: " .. arg) + end + end +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 +]] + +local function extract_embedded_jpeg(images) + + --[[ + check if the executable exists, since we can't do anything without it. check_if_bin_exists() + checks to see if there is a saved executable location. If not, it checks for the executable + in the user's path. When it finds the executable, it returns the command to run it. + ]] + + 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 = du.join({image.path, image.filename}, "/") + + --[[ + dtsys.external_command() is operating system aware and formats the command as necessary. + df.sanitize_filename() is used to quote the image filepath to protect from spaces. It is also + operating system aware and uses the corresponding quotes. + ]] + + 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) + copy_image_attributes(image, myimage, "all") + + --[[ + copy all of the tags except the darktable tags + ]] + + for _,tag in pairs(dt.tags.get_tags(image)) do + if not (string.sub(tag.name,1,9) == "darktable") then + dt.print_log("attaching tag") + dt.tags.attach(tag,myimage) + end + end + 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 + end + end + else + dt.print_error("ufraw-batch not found. Exiting...") -- print debugging error message + dt.print("ufraw-batch not found, exiting...") -- print the error to the screen + end +end + +--[[ + script_manager integration to allow a script to be removed + without restarting darktable +]] + +local function destroy() + dt.destroy_event("multi_os", "shortcut") -- destroy the event since the callback will no longer be present + dt.gui.libs.image.destroy_action("multi_os") -- remove the button from the selected images module +end +--[[ + Windows and MacOS don't place executables in the user's path so their location needs to be specified + so that the script can find them. An exception to this is packages installed on MacOS with homebrew. Those + executables are put in /usr/local/bin. These are saved as executable path preferences. check_if_bin_exists() + looks for the executable path preference for the executable. If it doesn't find one, then the path is checked + to see if the executable is there. +]] + +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]"), + value = df.get_executable_path_preference(executable), + is_directory = false, + changed_callback = function(self) + if df.check_if_bin_exists(self.value) then + df.set_executable_path_preference(executable, self.value) + end + end + } + 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 + "ufraw-batch", -- default + ufraw_batch_path_widget + ) +end + +--[[ + Add a button to the selected images module in lighttable +]] + +dt.gui.libs.image.register_action( + "multi_os", _("extract embedded jpeg"), + function(event, images) extract_embedded_jpeg(images) end, + _("extract embedded jpeg") +) + +--[[ + Add a shortcut +]] + +dt.register_event( + "multi_os", "shortcut", + function(event, shortcut) extract_embedded_jpeg(dt.gui.action_images) end, + _("extract embedded jpeg") +) + +--[[ + 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 = _("multi OS"), + purpose = _("example module thet runs on different operating systems"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/multi_os" +} + +script_data.destroy = destroy + +return script_data diff --git a/examples/panels_demo.lua b/examples/panels_demo.lua new file mode 100644 index 00000000..ac9e3de4 --- /dev/null +++ b/examples/panels_demo.lua @@ -0,0 +1,157 @@ +--[[ + This file is part of darktable, + copyright (c) 2019 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 . +]] +--[[ + panels_demo - an example script demonstrating how to contol panel visibility + + panels_demo is an example script showing how to control panel visibility. It cycles + through the panels hiding them one by one, then showing them one by one, then + hiding all, then showing all. Finally, the original panel visibility is restored. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + * none + + USAGE + * require this script from your main lua file + + BUGS, COMMENTS, SUGGESTIONS + * Send to Bill Ferguson, wpferguson@gmail.com + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- V E R S I O N C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("7.0.0", "panels_demo") + +-- script_manager integration to allow a script to be removed +-- without restarting darktable +local function destroy() + -- nothing to destroy +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE_NAME = "panels_demo" +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- T R A N S L A T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N +-- - - - - - - - - - - - - - - - - - - - - - - - + +-- alias dt.control.sleep to sleep +local sleep = dt.control.sleep + +local panels = {"DT_UI_PANEL_CENTER_TOP", -- center top panel + "DT_UI_PANEL_CENTER_BOTTOM", -- center bottom panel + "DT_UI_PANEL_TOP", -- complete top panel + "DT_UI_PANEL_LEFT", -- left panel + "DT_UI_PANEL_RIGHT", -- right panel + "DT_UI_PANEL_BOTTOM"} -- complete bottom panel + +local panel_status = {} + +-- save panel visibility + +for i = 1,#panels do + panel_status[i] = dt.gui.panel_visible(panels[i]) +end + +-- show all just in case + +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")) +sleep(1500) + +for i = 1, #panels do + dt.print(string.format(_("hiding %s"), panels[i])) + dt.gui.panel_hide(panels[i]) + sleep(1500) +end + +-- display left, then top, then right, then bottom + + dt.print(_("make panels visible, one at a time")) + sleep(1500) + + for i = #panels, 1, -1 do + dt.print(string.format(_("showing %s"), panels[i])) + dt.gui.panel_show(panels[i]) + sleep(1500) + end + +-- hide all + +dt.print(_("hiding all panels")) +sleep(1500) + +dt.gui.panel_hide_all() +sleep(1500) + +-- show all + +dt.print(_("showing all panels")) +sleep(1500) + +dt.gui.panel_show_all() +sleep(1500) + +-- restore + +dt.print(_("restoring panels to starting configuration")) +for i = 1, #panels do + if panel_status[i] then + dt.gui.panel_show(panels[i]) + else + dt.gui.panel_hide(panels[i]) + end +end + +-- 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 = _("panels demo"), + purpose = _("example demonstrating how to contol panel visibility"), + author = "Bill Ferguson ", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/panels_demo" +} + +script_data.destroy = destroy + +return script_data diff --git a/examples/preferenceExamples.lua b/examples/preferenceExamples.lua index 7ac1cfd1..84425744 100644 --- a/examples/preferenceExamples.lua +++ b/examples/preferenceExamples.lua @@ -22,60 +22,88 @@ USAGE * require this script from your main lua file ]] local dt = require "darktable" -dt.configuration.check_version(...,{2,0,1},{3,0,0},{4,0,0},{5,0,0}) +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. - "preferenceExamplesString", -- name + "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. - "preferenceExamplesString", -- name + "preferenceExamplesInteger", -- name "integer", -- type - "Example Integer", -- label - "Example Integer Tooltip", -- tooltip + _("example") .. " integer", -- label + _("example") .. " integer " .. _("tooltip"), -- tooltip 2, -- default 1, -- min 99) -- max 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 + "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 0.5) -- step 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 + "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. - "preferenceExamplesString", -- name + "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. - "preferenceExamplesString", -- name + "preferenceExamplesEnum", -- name "enum", -- type - "Example Enum", -- label - "Example Enum Tooltip", -- tooltip + _("example") .. " enum", -- label + _("example") .. " enum " .. _("tooltip"), -- tooltip "Enum 1", -- default "Enum 1", "Enum 2") -- values + +local function destroy() + -- nothing to destroy +end + +script_data.destroy = destroy + +return script_data diff --git a/examples/printExamples.lua b/examples/printExamples.lua index 5ef3695e..721138cd 100644 --- a/examples/printExamples.lua +++ b/examples/printExamples.lua @@ -20,11 +20,27 @@ USAGE * require this file from your main lua config file: ]] local dt = require "darktable" -dt.configuration.check_version(...,{5,0,0}) +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() + -- nothing to destroy +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 @@ -36,5 +52,20 @@ dt.print_error("print error") -- to enable the Lua logdomain. dt.print_log("print log") +-- 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 = _("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 -- -- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua diff --git a/examples/running_os.lua b/examples/running_os.lua index a615fa9a..69741288 100644 --- a/examples/running_os.lua +++ b/examples/running_os.lua @@ -26,9 +26,41 @@ prints the operating system ]] local dt = require "darktable" -dt.configuration.check_version(...,{5,0,0}) +local du = require "lib/dtutils" -dt.print("You are running: "..dt.configuration.running_os) +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)) + +-- 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 -- -- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua diff --git a/examples/x-touch.lua b/examples/x-touch.lua new file mode 100644 index 00000000..7da3d54c --- /dev/null +++ b/examples/x-touch.lua @@ -0,0 +1,208 @@ +--[[ + This file is part of darktable, + copyright (c) 2023 Diederik ter Rahe + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +]] +--[[ +X-Touch Mini flexible encoder shortcuts + +This script will create virtual sliders that are mapped dynamically to +the most relevant sliders for the currently focused processing module. +Tailored modules are color zones, tone equalizer, color calibration and +mask manager properties. The script can easily be amended for other +devices or personal preferences. Virtual "toggle" buttons can be created +as well, that dynamically change meaning depending on current status. + +USAGE +* require this script from your main lua file +* restart darktable +* create shortcuts for each of the encoders on the x-touch mini + to a virtual slider under lua/x-touch + or import the following shortcutsrc file in the shortcuts dialog/preferences tab: + +None;midi:CC1=lua/x-touch/knob 1 +None;midi:CC2=lua/x-touch/knob 2 +None;midi:CC3=lua/x-touch/knob 3 +None;midi:CC4=lua/x-touch/knob 4 +None;midi:CC5=lua/x-touch/knob 5 +None;midi:CC6=lua/x-touch/knob 6 +None;midi:CC7=lua/x-touch/knob 7 +None;midi:CC8=lua/x-touch/knob 8 +midi:E0=global/modifiers +midi:F0=global/modifiers;ctrl +midi:F#0=global/modifiers;alt +midi:G#-1=iop/blend/tools/show and edit mask elements +midi:A-1=iop/colorzones;focus +midi:A#-1=iop/toneequal;focus +midi:B-1=iop/colorbalancergb;focus +midi:C0=iop/channelmixerrgb;focus +midi:C#0=iop/colorequal;focus +midi:D0=iop/colorequal/page;previous +midi:D#0=iop/colorequal/page;next +]] + +local dt = require "darktable" +local du = require "lib/dtutils" + +du.check_min_api_version("9.2.0", "x-touch") + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + +-- return data structure for script_manager + +local script_data = {} + +script_data.metadata = { + name = _("x-touch"), + purpose = _("example of how to control an x-touch midi device"), + author = "Diederik ter Rahe", + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/x-touch" +} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +-- set up 8 mimic sliders with the same function +for k = 1,8 do + dt.gui.mimic("slider", "knob ".. k, + function(action, element, effect, size) + -- take the number from the mimic name + local k = tonumber(action:sub(-1)) + + -- only operate in darkroom; return NAN otherwise + if dt.gui.current_view() ~= dt.gui.views.darkroom then + return 0/0 + end + + local maskval = 0/0 + if k < 8 then + -- first try if the mask slider at that position is active + local s = { "opacity", + "size", + "feather", + "hardness", + "rotation", + "curvature", + "compression" } + maskval = dt.gui.action("lib/masks/properties/" .. s[k], + element, effect, size) + end + -- if a value different from NAN is returned, the slider was active + if maskval == maskval then + return maskval + + -- try if colorzones module is focused; if so select element of graph + elseif dt.gui.action("iop/colorzones", "focus") ~= 0 then + which = "iop/colorzones/graph" + local e = { "red", + "orange", + "yellow", + "green", + "aqua", + "blue", + "purple", + "magenta" } + element = e[k] + + -- try if colorequalizer module is focused; if so select element of graph + elseif dt.gui.action("iop/colorequal", "focus") ~= 0 then + local e = { "red", + "orange", + "yellow", + "green", + "cyan", + "blue", + "lavender", + "magenta" } + which = "iop/colorequal/graph" + element = e[k] + + -- if the sigmoid rgb primaries is focused, + -- check sliders + elseif dt.gui.action("iop/sigmoid", "focus") ~= 0 and k <8 then + local e = { "red attenuation", "red rotation", "green attenuation", "green rotation", "blue attenuation", "blue rotation", "recover purity" } + which = "iop/sigmoid/primaries/"..e[k] + + -- if the rgb primaries is focused, + -- check sliders + elseif dt.gui.action("iop/primaries", "focus") ~= 0 and k >=1 then + local e = { "red hue", "red purity", "green hue", "green purity", "blue hue", "blue purity", "tint hue", "tint purity" } + which = "iop/primaries/" ..e[k] + + -- if the tone equalizer is focused, + -- select one of the sliders in the "simple" tab + elseif dt.gui.action("iop/toneequal", "focus") ~= 0 then + which ="iop/toneequal/simple/"..(k-9).." EV" + + -- if color calibration is focused, + -- the last 4 knobs are sent there + elseif dt.gui.action("iop/channelmixerrgb", "focus") ~= 0 + and k >= 5 then + -- knob 5 selects the active tab; pressing it resets to CAT + if k == 5 then + which = "iop/channelmixerrgb/page" + element = "CAT" + -- since the tab action is not a slider, + -- the effects need to be translated + if effect == "up" then effect = "next" + elseif effect == "down" then effect = "previous" + else effect = "activate" + end + else + -- knobs 6, 7 and 8 are for the three color sliders on each tab + which = "iop/focus/sliders" + local e = { "1st", + "2nd", + "3rd" } + element = e[k - 5] + end + + -- the 4th knob is contrast; + -- either colorbalance if it is focused, or filmic + elseif dt.gui.action("iop/colorbalancergb", "focus") ~= 0 + and k == 4 then + which = "iop/colorbalancergb/contrast" + + -- in all other cases use a default selection + else + local s = { "iop/exposure/exposure", + "iop/filmicrgb/white relative exposure", + "iop/filmicrgb/black relative exposure", + "iop/filmicrgb/contrast", + "iop/crop/left", + "iop/crop/right", + "iop/crop/top", + "iop/crop/bottom" } + which = s[k] + end + + -- now pass the element/effect/size to the selected slider + return dt.gui.action(which, element, effect, size) + end) +end + +local function destroy() + -- nothing to destroy +end + +script_data.destroy = destroy + +return script_data diff --git a/include_all.lua b/include_all.lua deleted file mode 100644 index cbe3b2b2..00000000 --- a/include_all.lua +++ /dev/null @@ -1,54 +0,0 @@ ---[[ - This file is part of darktable, - copyright (c) 2014 Jérémy Rosen - - 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 . -]] ---[[ -INCLUDE ALL -Automatically include all scripts in the script repository - -This is intended for debugging purpose - - -USAGE -* require this file from your main lua config file: -* go to configuration => preferences -* Enable the scripts you want to use -* restart darktable - -Note that you need to restart DT for your changes to enabled scripts to take effect - -]] -local dt = require "darktable" -local io = require "io" -dt.configuration.check_version(...,{3,0,0}) - -local output = io.popen("cd "..dt.configuration.config_dir.."/lua ;find . -name \\*.lua -print") -local my_name={...} -my_name = my_name[1] -for line in output:lines() do - local req_name = line:sub(3,-5) - if req_name ~= my_name then - dt.preferences.register(my_name,req_name,"bool","enable "..req_name, - "Should the script "..req_name.." be enabled at next startup",false) - - if dt.preferences.read(my_name,req_name,"bool") then - require(req_name) - end - end -end - --- --- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua diff --git a/lib/dtutils.lua b/lib/dtutils.lua index 02888a8e..e6595865 100644 --- a/lib/dtutils.lua +++ b/lib/dtutils.lua @@ -32,7 +32,78 @@ local dt = require "darktable" local log = require "lib/dtutils.log" -dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0}) +dtutils.libdoc.functions["check_min_api_version"] = { + Name = [[check_min_api_version]], + Synopsis = [[check the minimum required api version against the current api version]], + Usage = [[local du = require "lib/dtutils" + + local result = du.check_min_api_version(min_api, script_name) + min_api - string - the api version that the application was written for (example: "5.0.0") + script_name - string - the name of the script]], + Description = [[check_min_api_version compares the minimum api required for the appllication to + run against the current api version. The minimum api version is typically the api version that + was current when the application was created. If the minimum 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. + + This function is intended to replace darktable.configuration.check_version(). The application code + won't have to be updated each time the api changes because this only checks the minimum version required.]], + Return_Value = [[result - true if the minimum api version is available, false if not.]], + Limitations = [[When using the default handler on a script being executed from the luarc file, the error thrown + will stop the luarc file from executing any remaining statements. This limitation does not apply to script_manger.]], + Example = [[check_min_api_version("5.0.0") does nothing if the api is greater or equal to 5.0.0 otherwise an + error message is printed and an error is thrown stopping execution of the script.]], + See_Also = [[]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +function dtutils.check_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 + dt.print_error("This application is written for lua api version " .. min_api .. " or later.") + dt.print_error("The current lua api version is " .. current_api) + dt.print("ERROR: " .. script_name .. " failed to load. Lua API version " .. min_api .. " or later required.") + dt.control.sleep(2000) -- allow time for the error to display before script_manager writes it's error message + error("Minimum API " .. min_api .. " not met for " .. script_name .. ".", 0) + end +end + +dtutils.libdoc.functions["check_max_api_version"] = { + Name = [[check_max_api_version]], + Synopsis = [[check the maximum required api version against the current api version]], + Usage = [[local du = require "lib/dtutils" + + local result = du.check_max_api_version(max_api, script_name) + max_api - string - the api version that the application was written for (example: "5.0.0") + script_name - string - the name of the script]], + Description = [[check_max_api_version compares the maximum api required for the appllication to + run against the current api version. This function is used when a part of the Lua API that + the script relies on is removed. If the maximum api version is not met, then an + error message is printed saying the script_name failed to load, then an error is thrown causing the + program to stop executing.]], + Return_Value = [[result - true if the maximum api version is available, false if not.]], + Limitations = [[When using the default handler on a script being executed from the luarc file, the error thrown + will stop the luarc file from executing any remaining statements. This limitation does not apply to script_manger.]], + Example = [[check_max_api_version("9.0.0") does nothing if the api is less than or equal to 9.0.0 otherwise an + error message is printed and an error is thrown stopping execution of the script.]], + See_Also = [[]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +function dtutils.check_max_api_version(max_api, script_name) + local current_api = dt.configuration.api_version_string + if dtutils.compare_api_versions(current_api, max_api) > 0 then + dt.print_error("This application is written for lua api version " .. max_api .. " or earlier.") + dt.print_error("The current lua api version is " .. current_api) + dt.print("ERROR: " .. script_name .. " failed to load. Lua API version " .. max_api .. " or earlier required.") + dt.control.sleep(2000) -- allow time for the error to display before script_manager writes it's error message + error("Maximum API " .. max_api .. " not met for " .. script_name .. ".", 0) + end +end dtutils.libdoc.functions["split"] = { Name = [[split]], @@ -149,7 +220,8 @@ function dtutils.prequire(req_name) if status then log.msg(log.info, "Loaded " .. req_name) else - log.msg(log.info, "Error loading " .. req_name) + log.msg(log.error, "Error loading " .. req_name) + log.msg(log.error, "Error returned is " .. lib) end return status, lib end @@ -215,4 +287,181 @@ function dtutils.spairs(_table, order) -- Code copied from http://stackoverflow. end end +dtutils.libdoc.functions["check_os"] = { + Name = [[check_os]], + Synopsis = [[check that the operating system is supported]], + Usage = [[local du = require "lib/dtutils" + + local result = du.check_os(operating_systems) + operating_systems - a table of operating system names such as {"windows","linux","macos","unix"}]], + Description = [[check_os checks a supplied table of operating systems against the operating system the + script is running on and returns true if the OS is in the list, otherwise false]], + Return_Value = [[result - boolean - true if the operating system is supported, false if not.]], + Limitations = [[]], + Example = [[local du = require "lib/dtutils" + if du.check_os({"windows"}) then + -- run the script + else + dt.print("Script