343 Commits

Author SHA1 Message Date
wpferguson
96ba075e86 Removed debugging statement 2020-12-22 22:00:41 -05:00
Bill Ferguson
e86a3a3236 [file.lua] removed debugging statement 2020-12-18 11:08:12 -05:00
Bill Ferguson
b41b2a1566 [file.lua] Added create_tmp_file() back in. 2020-11-30 21:51:55 -05:00
Bill Ferguson
b893383353 [file.lua] Added comments for local functions. Added more API
documentation for check_if_bin_exists.  Added save
           executable path preference to _search_for_nix_bin()
           so that we can save a little time on subsequent runs
           once an executable is found.
2020-11-30 21:06:57 -05:00
Bill Ferguson
ddcf3ed3f4 [file.lua] fixed line trimming in macos search for bin 2020-11-29 18:41:32 -05:00
Bill Ferguson
da05748393 [file.lua] combined the is_ functions into file_test() which allows
testing to see if a path exists, is a dir, is a file, or
	   is executable.  Added case insensitive search to the
           windows search for bin.
2020-11-29 18:00:47 -05:00
Bill Ferguson
7790c9700b Separated check_if_bin_exists() into 2 functions, one to check the returned
executable_path variable and another to search for the binary if it
wasn't detected.  All results are checked to ensure they exist, are
a file, and are executable.  This fixes the situation where a directory
gets stored as an executable path. Search functions are operating system
specific.  Executable paths are saved for MacOS and Windows to make for
shorter searches in the future.
2020-08-02 18:33:49 -04:00
Bill Ferguson
1d5192a39c removed duplpicate copies of the is_ functions 2020-07-21 20:40:22 -04:00
Bill Ferguson
04525e1258 Added functions is_file(path), is_dir(path, and is_executable(path) to
ensure a path is a file, directory, or executable respectively.  Replaced
code in check_if_bin_exists with these functions to reduce the lines of code
and make it more understandable.
2020-07-21 16:39:34 -04:00
Bill Ferguson
4c536b41bc checkpoint 2020-06-08 12:45:49 -04:00
wpferguson
55ced3ecc7 Merge pull request #251 from wpferguson/fix_script_manager_scrolling
Make script manager scrollable.
2020-05-23 17:01:52 -04:00
Bill Ferguson
4a81b560f9 Changed position of script manager to the center section of the left
panel at the bottom of the section.  This takes care of the problem
of darktable exceeding the vertical screen size and not being
scrollable.
2020-05-23 16:54:24 -04:00
wpferguson
6a7cd31ab4 Merge pull request #249 from wpferguson/fix_moduleExample
Update examples/moduleExample.lua
2020-05-23 16:18:07 -04:00
wpferguson
2515db31d8 Merge pull request #250 from sbolten/enhance_face_recognition
face_recognition.lua: Added "People" category to tag routine
2020-05-09 23:13:38 -04:00
Stephan Bolten
54a66f9ab2 Category Tag default & no_person_found
1 - empty by default
2 - not added when no_person_found
2020-05-09 19:58:57 +02:00
Bill Ferguson
0856858e73 Updated comments to reflect current usage practices. Added/updated links to the online manual for each of the widgets. Added translation. Removed reference to api_major_version 6 2020-05-08 00:15:31 -04:00
Stephan Bolten
d579b257a8 Added "People" category to tag routine
Added field in GUI to provide "People" category


Changed label of tag category


Changed Tooltip
2020-05-06 18:57:50 +02:00
wpferguson
8a8f6338c5 Merge pull request #243 from piterdias/master
Fixes face_recognition.lua failing to run in a fresh installation of darktable and adds some more logs
2020-04-27 15:05:14 -04:00
Piter Dias
71207e8f57 1 - face_recognition.lua fails to run in a fresh installation of darktable. It is due to pull request #221 where reset_preferences() and save_preferences() are called before being defined. This is actually a expected behavior because compiles script is one pass instead of two pass (like some other languages, like C). Moving a small piece of preferences setting code to the end of file solves the issue, because all required objects and functions are defined at that point.
2 - Add some more logs
2020-04-25 19:57:14 -03:00
supertobi
749f5b8e8f Merge pull request #240 from preklov/patch-2
German translation of OpenInExplorer.lua
2020-04-21 13:15:34 +02:00
wpferguson
88c1be09c2 Merge pull request #239 from preklov/master
Enabled macOS Finder in OpenInExplorer.lua
2020-04-20 13:55:51 -04:00
preklov
c1b59256be Add files via upload
The translation of the finished version of OpenInExplorer.lua
2020-04-20 16:14:32 +02:00
preklov
a3f57665a9 Delete OpenInExplorer.po 2020-04-20 16:13:05 +02:00
preklov
b8d29ca831 Add files via upload
The new and I hope the finished version for now.
2020-04-20 16:06:38 +02:00
preklov
90880ff4d1 Delete OpenInExplorer.lua 2020-04-20 16:03:25 +02:00
wpferguson
0bb8b72183 Merge pull request #241 from piterdias/face_recognition
Solve stability and performance issues on face_recognition.lua
2020-04-17 15:31:05 -04:00
preklov
91ab85c825 Update OpenInExplorer.lua 2020-04-16 14:07:41 +02:00
Piter Dias
8c75b9316d Rollback some indentation changes 2020-04-16 01:46:45 -03:00
Piter Dias
9870ddbe86 face_recognition.lua script face some stability and performance issues on an i5-6600, 32 GB and GTX 1060 6GB:
1. Taking to much time – almost a day for 3000 images
    2. Crashing due to lines malformation in facerecognition.txt – some times face_recognition Python script concatenates two output lines in just once, what is not expected by face_recognition.lua script
    3. Crashing the whole system in Linux – I keep receiving “bash: fork: resource temporarily unavailable” trying to process a too big (starting from some hundreds) image list. Some other applications starts to crash at this point. It seems related to the attach tag call because commenting this instruction (for debugging purposes) eliminates the crashes.
After debugging the Lua script and checking Lua and SQL darktable out logs, I decided to make the following changes:
    1. Remove an inner loop in the processing results step – helps preventing issue 1
    2. Remove a loop in the export step – helps preventing issue 1
    3. Remove duplicate tags (when there are more more than a reference face for each person) while facerecognition.txt is read – helps preventing issue 1 and 3
    4. Make a sanity check with processing images, skip any malformation data – helps preventing issue 2
    5. Reduces the sqlite access by caching tags that where already created for a previous image – helps preventing issue 3

Those changes allowed may thousands (more than 30K) of images to be processed in half a day instead of several days.
2020-04-15 14:03:58 -03:00
supertobi
c7eb101920 Updated the "USAGE" section in the header of enfuseAdvanced.lua 2020-04-15 17:42:42 +02:00
preklov
2b171aef7c German translation of OpenInExplorer.lua 2020-04-14 19:33:54 +02:00
preklov
ec0fe0b047 Rename OpenInExplorer.lua to contrib/OpenInExplorer.lua 2020-04-14 16:15:45 +02:00
preklov
bbfa81f051 Add files via upload 2020-04-14 15:46:41 +02:00
preklov
bd5bef279c Delete OpenInExplorer.lua 2020-04-14 15:36:48 +02:00
preklov
afa95d5d34 Update OpenInExplorer.lua 2020-04-14 15:26:28 +02:00
preklov
fb331710e1 Add files via upload 2020-04-14 15:07:22 +02:00
preklov
224dce1b76 Delete OpenInExplorer.lua 2020-04-14 15:04:31 +02:00
wpferguson
a582f0229f Merge pull request #231 from Mark-64/RL_out_sharp
Add RL output sharpening script
2020-03-22 14:53:26 -04:00
U-DESKTOP-HQME86J\marco
ebd33c4c16 changed default sigma 2020-03-22 17:21:30 +01:00
wpferguson
3f5cf06bd9 Merge pull request #230 from Mark-64/fix_lib
Sanitize filepath before checking for its existance in Windows (fix #229)
2020-03-21 20:58:49 -04:00
wpferguson
1ec9cfb772 Merge pull request #234 from schwerdf/master
New script: 'transfer_hierarchy'.
2020-03-16 11:10:06 -04:00
August Schwerdfeger
050175802f Repositioned the 'transfer hierarchy' module below 'select' and 'selected image[s]'; added a translatability layer for strings; corrected behavior in the case of images with multiple versions. 2020-03-15 22:39:07 -05:00
August Schwerdfeger
19a1be1f74 Corrected a variable name. 2020-03-14 00:28:53 -05:00
August Schwerdfeger
093593dcef New script: 'transfer_hierarchy'. 2020-03-13 23:49:24 -05:00
U-DESKTOP-HQME86J\marco
51f28a386b requested modifications 2020-03-11 16:53:55 +01:00
U-DESKTOP-HQME86J\marco
4065bd5e11 test for GMIc executable 2020-03-10 21:21:41 +01:00
U-DESKTOP-HQME86J\marco
3e1746fc6e changed min API version 2020-03-10 20:34:13 +01:00
U-DESKTOP-HQME86J\marco
3b6dd856aa Add RL output sharpening script 2020-03-10 12:54:53 +01:00
U-DESKTOP-HQME86J\marco
635ea3f231 fix #229 2020-03-07 08:57:03 +01:00
wpferguson
48d1c62fed Merge pull request #227 from j3ffrw/master
enfuse output filename and blend colorspace
2020-03-02 11:55:27 -05:00
jefffermo@gmail.com
e97f2f3fc6 * make enfuse output file unique(fairly)
* add option --blend-colorspace of situation like this https://monochrome.sutic.nu/2019/06/18/enblend-4.2-color-space-blending-bug.html
2020-02-27 13:26:37 +08:00
supertobi
19b016a3dd Merge pull request #225 from Mark-64/ext_editor_v3
allow more than 9 editors
2020-02-09 20:14:25 +01:00
Marco
576e160372 Merge branch 'master' into ext_editor_v3 2020-02-07 21:26:47 +01:00
U-DESKTOP-HQME86J\marco
e586e0479d allow more that 9 editors 2020-02-07 21:13:19 +01:00
supertobi
0b5c866df6 Merge pull request #223 from wpferguson/remove_include_all
Remove include_all.lua
2020-01-29 09:23:17 +01:00
wpferguson
9828fb3934 Merge pull request #224 from wpferguson/readme_update
Update README.md
2020-01-28 14:16:52 -05:00
Bill Ferguson
430f443916 Added Stefan Klinger's darktable lua scripts repository 2020-01-28 14:14:47 -05:00
Bill Ferguson
d32e0eb5ab Removed include_all.lua since we now run cross platform and not all scripts run on all operating systems. 2020-01-28 12:11:33 -05:00
wpferguson
f72750576f Merge pull request #221 from piterdias/face_recognition_no_persons_found
Add "no persons found" entry and other enhancements
2020-01-27 17:51:52 -05:00
Piter Dias
e96b78b175 Add reset callback to face recognition widget and show the default values in the GUI. 2020-01-27 17:44:49 -03:00
Piter Dias
4708110804 1. Add "no persons found" entry for the situations that no persons are
detected in the photos
2. Check if the tags to applied are non empty and skip otherwise. It
applies to empty entries in the GUI ("unknown person tag" and "no
persons found"), when the user types something and then deletes it
3. Change the string "togs of images to ignore" to "tags of images to
ignore"
2020-01-23 21:12:49 -03:00
wpferguson
2725b82543 Merge pull request #220 from wpferguson/face_recognition_fix
Fix face_recognition crash
2020-01-22 13:18:01 -05:00
Bill Ferguson
1eafae6c6f Add check for WARNING lines complaining about files with no faces in the faces directory 2020-01-22 13:15:14 -05:00
wpferguson
11e25a21fa Merge pull request #218 from schwerdf/master
Correct argument escaping in 'dtutils_string.sanitize'.
2020-01-22 12:27:20 -05:00
August Schwerdfeger
51eda4e755 Made Windows- and POSIX-specific sanitization functions local; moved the 'sanitize' functions after the 'is_not_sanitized' functions that they call. 2020-01-21 22:17:12 -06:00
August Schwerdfeger
847c39749d Corrected escaping of command-line arguments in the 'sanitize' and 'is_not_sanitized' functions. 2020-01-21 00:29:18 -06:00
wpferguson
cb0975fa69 Merge pull request #217 from wpferguson/library_issues
Fix embedded quotes in strings not sanitized
2020-01-19 23:21:44 -05:00
Bill Ferguson
5b977733b3 Rewrote sanitize and is_not_sanitized to handle strings with embedded operatings system quotes. Fixes #216. 2020-01-19 23:11:41 -05:00
supertobi
c3d13a9890 Merge pull request #215 from Mark-64/error_fix
New version of ext-editor
2020-01-19 20:40:07 +01:00
U-DESKTOP-HQME86J\marco
bc4c04f795 error fix 2020-01-18 12:45:58 +01:00
wpferguson
32d5240e5d Merge pull request #214 from wpferguson/enfuse_bin_check
enfuse official - added binary found check
2020-01-05 16:57:24 -05:00
Bill Ferguson
64f423f3d1 Added a check to ensure the enfuse executable is found. If the executable isn't found, throw an error, print messgaes, and refuse to load. 2020-01-05 15:33:53 -05:00
supertobi
18d0e974e4 Merge pull request #211 from Yvan-56/patch-1
Update face_recognition for tolerance and locales
2020-01-04 19:51:15 +01:00
supertobi
c259f2dc72 Merge pull request #207 from Mark-64/ext_editor_v2
contrib/ext_editor version 2
2020-01-04 19:45:17 +01:00
Yvan-56
5223fdf4ae Update face_recognition for tolerance and locales
When locales use comma for numeric decimal separator, tolerance argument is not passed correctly.
I propose to switch temporally to "C" locales before converting float tolerance to string to get a period decimal separator.
2020-01-04 16:18:46 +01:00
Marco
a4132bd9bc Merge branch 'master' into ext_editor_v2 2020-01-04 12:22:24 +01:00
U-DESKTOP-HQME86J\marco
bf8d147ca5 requeste changes 2020-01-02 19:09:19 +01:00
U-DESKTOP-HQME86J\marco
5116d645db further improvements 2020-01-01 19:27:41 +01:00
U-DESKTOP-HQME86J\marco
a2850cac6b contrib/ext_editor v2 2019-12-30 14:07:42 +01:00
supertobi
ffed4efdec Merge pull request #204 from Mark-64/ext_editor
Add ext_editor.lua to contrib
2019-12-25 22:07:45 +01:00
U-DESKTOP-HQME86J\marco
cfa71927de requested updates 2019-12-25 16:16:44 +01:00
U-DESKTOP-HQME86J\marco
5d7a60206e requested updates 2019-12-25 16:02:53 +01:00
wpferguson
3556fef5ed Merge pull request #205 from wpferguson/dt3.0_readme_updates
Updated README.md to reflect latest changes
2019-12-24 11:54:37 -05:00
Bill Ferguson
252965a5d9 Updated README.md to reflect latest changes 2019-12-24 11:52:56 -05:00
Bill Ferguson
a7a25c0798 Merge branch 'wpferguson-remove_video_mencoder' 2019-12-24 11:43:39 -05:00
Bill Ferguson
944087649f removed deprecated contrib/video_mencoder.lua 2019-12-24 11:43:01 -05:00
wpferguson
69c9db6dda Merge pull request #203 from wpferguson/dt30examples
DT 3.0 lua API change demonstration
2019-12-24 11:37:28 -05:00
wpferguson
f5e86a8d25 Merge pull request #147 from simon04/delete-mo
Delete compiled .mo files
2019-12-24 11:37:02 -05:00
U-DESKTOP-HQME86J\marco
6a89357b54 compatibility with Linux 2019-12-23 11:34:01 +01:00
U-DESKTOP-HQME86J\marco
64c8f8fc3e added darkroom mode 2019-12-22 21:08:32 +01:00
U-DESKTOP-HQME86J\marco
54ff56b021 added copy and edit button + code cleaning 2019-12-21 21:12:46 +01:00
U-DESKTOP-HQME86J\marco
3d3ad89fba Add ext_editor.lua to contrib 2019-12-20 23:00:57 +01:00
Bill Ferguson
7ee140c8c8 created 3 demonstration scripts of the dt3.0 lua API additions 2019-12-19 13:38:25 -05:00
wpferguson
8afd6ccee4 Merge pull request #202 from wpferguson/fix_libraries
fix libraries
2019-12-16 22:45:59 -05:00
Bill Ferguson
e427294ec1 fix library problems found during unit testing 2019-12-16 22:42:32 -05:00
wpferguson
18283ccbc6 Merge pull request #201 from darktable-org/revert-199-library_fixes
Revert "Library fixes"
2019-12-16 22:31:40 -05:00
wpferguson
b7857c8462 Revert "Library fixes" 2019-12-16 22:30:50 -05:00
wpferguson
2d231806d2 Merge pull request #199 from wpferguson/library_fixes
Library fixes
2019-12-16 21:46:14 -05:00
wpferguson
36b58333ca Merge pull request #200 from wpferguson/README_update
Update README.md
2019-12-16 21:40:26 -05:00
Bill Ferguson
fc77af598d Another update 2019-12-16 20:39:10 -05:00
Bill Ferguson
602295ae89 Updated contributed scripts 2019-12-16 20:27:20 -05:00
Bill Ferguson
1c295a88ac Fixed bugs found during unit testing with testing framework 2019-12-11 22:55:44 -05:00
wpferguson
232535faf9 Merge pull request #197 from wpferguson/update_face_recognition
Update face recognition
2019-12-09 20:19:24 -05:00
Bill Ferguson
10b5dee40f Added windows compatibility 2019-12-09 20:08:11 -05:00
Bill Ferguson
be48459092 Converted face_recognition to a module and updated it to use newer library functions. Added tolerance adjustment to settings to fine tune the face detection. 2019-12-08 20:18:07 -05:00
supertobi
cb6199c1a8 Merge pull request #100 from GHswitt/master
Storage plugin for face_recognition
2019-12-07 21:34:38 +01:00
supertobi
c9eb087ade Update README.md
The ' is not not needed in the windows cmd
2019-12-04 13:01:58 +01:00
supertobi
87e223a854 Update README.md
Added: 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.
2019-12-04 12:52:44 +01:00
wpferguson
7c7118dfad Merge pull request #193 from simon04/patch-1
README: list various third-party projects
2019-11-10 00:33:39 -05:00
Simon Legner
fdcd6c6dfe README: list various third-party projects 2019-11-04 21:42:15 +01:00
Bill Ferguson
205cbcf05d Checkpoint save 2019-07-03 12:41:58 -04:00
wpferguson
c9ccbb2750 Merge pull request #189 from wpferguson/remove_plugin_manager_symlink
Removed symlink for plugin_manager development
2019-06-19 11:25:32 -04:00
Bill Ferguson
eacfc4b623 Removed symlink for plugin_manager development 2019-06-19 11:23:11 -04:00
supertobi
d3dafd4ce1 Merge pull request #179 from BzKevin/BzKevin-enfuseAdvanced-refactor
Refactored enfuseAdvanced
2019-05-07 15:56:06 +02:00
supertobi
8004b96c41 Merge branch 'master' into BzKevin-enfuseAdvanced-refactor 2019-05-07 15:54:12 +02:00
supertobi
7cc58d9fac Merge pull request #164 from BzKevin/BzKevin-HDRMerge
Add HDRMerge
2019-05-07 13:00:40 +02:00
BzKevin
f82ae6c091 Change 'Create Unique Filename' to default 2019-05-06 21:35:41 -04:00
wpferguson
6ab029e692 Merge pull request #178 from BzKevin/BzKevin-AutoGrouper
Add AutoGrouper
2019-05-06 15:38:10 -04:00
BzKevin
2c3536afc9 Changes requestd via PR
1) mod changed to MOD
2) autogroup changed to autogrouper
3) prints "please select at least 2 images" if user attempts to run with fewer than 2 images selected
4) change visible module name to "auto group" (lower case)
5) changed soft max on slider to 60s
         - I also increase the hard max up to a full day, I saw no reason not to
2019-05-06 10:49:30 -04:00
supertobi
0fe95e91d5 Merge pull request #185 from darktable-org/file.lua
file.lua copy for windows
2019-04-16 11:00:19 +02:00
supertobi
cadd98dc59 dtutils_file.file_copy: use the result 2019-04-16 10:58:32 +02:00
wpferguson
92f99c3780 Merge pull request #184 from darktable-org/dt_utils_system
On Windows we don't need anything to open a file
2019-04-16 00:03:12 -04:00
supertobi
f1d397f908 OpenInExplorer
Changed from nautilus to org.freedesktop.FileManager1, so it works with all file manager that support this dbus API.
2019-04-15 23:09:01 +02:00
supertobi
eeaa6b478b Merge pull request #171 from darktable-org/supertobi-patch-1
make kml_export multi OS
2019-04-15 21:35:03 +02:00
supertobi
53d6b54de8 Merge branch 'master' into supertobi-patch-1 2019-04-15 21:34:44 +02:00
supertobi
ffd6abbe95 Fixed bugs after Linux testing 2019-04-15 21:28:48 +02:00
supertobi
f08421a48c Update kml_export.lua
Fixed Windows problem
2019-04-15 15:55:22 +02:00
supertobi
a383d1ebf8 Update kml_export.lua
Works with Windows
2019-04-15 15:48:31 +02:00
supertobi
d355095e16 file.lua copy for windows 2019-04-15 15:27:53 +02:00
supertobi
a81d992552 On Windows we don't need anything to open a file
On Windows we don't need any command to open a file. 
And "start" has problems with spaces in the filename, even if we put quoter around them:
https://stackoverflow.com/questions/13691827/opening-file-with-spaces-in-windows-via-command-prompt
2019-04-15 14:27:17 +02:00
wpferguson
eb13518b96 Merge pull request #183 from wpferguson/executable_manager_fix
executable manager windows string spllit bug
2019-03-28 20:17:57 -04:00
Bill Ferguson
bf6e180291 Fixed bug that caused the lua executale path to not split correctly on windows. Preferences are stored internally with forward slashes, not O/S specific path separators. 2019-03-28 20:13:08 -04:00
BzKevin
fcad7a5733 Makte Translatable + others
Made text translatable except for line 499 which kept giving me problems
   Making L499 translatable would throw the following error   lua:499: attempt to call a nil value (upvalue '_')
   Troubleshooting steps taken and results:
        1) Changed the translation function from _() to translate() - this worked, no errors but ugly
        2) Reverted the changes from 1, then changed all "for _, var in table do" to be "for ignore, var in table do" - this did not help

Moved a chunk of code due to anomalous behavior, I suspect previous code was attempting to change the value in the stack widget before it was fully created. Adding in a print statement before trying to change the stack.active would cause things to work fine. Moving it behind the register call also helped.
2019-03-22 12:01:56 -04:00
BzKevin
7ffdf1631c Make text translatable 2019-03-22 09:05:11 -04:00
BzKevin
a5ad5e67f3 Remove executable preferences
The executable choosers have been moved to a box which is now element 4 in the GUI stack. Once the user configures these and presses update they are confirmed good or bad and the GUI changed to normal view if all works out.
2019-03-12 13:04:21 -04:00
BzKevin
4b3b1f7786 Fix bug when updating exe preference
Move the preference.write() call inside the for loop
2019-03-12 12:42:37 -04:00
BzKevin
844e89a652 Use file chooser to set executable path preference
previous commit failed to actually utilize the newly created file chooser to set the path preference
2019-03-12 12:25:37 -04:00
BzKevin
d88af9ffd9 Minor tweaks to last commit 2019-03-12 11:20:04 -04:00
BzKevin
7a3a9ba559 Removed preferences for exe
The GUI is now made of a stack which has two boxes in it. One box contains all the normal GUI elements, the other contains a file chooser button and a normal button to call an update routine. This box is only displayed if an issue is found. I decided to track the install status via a preference boolean, rather than calling df_check_if_bin_exists at startup every time. I do this because on my windows PC I was starting to get slow startups with too many scripts installed and many many flashing CMD windows at startup as well, which was a nuisance.
2019-03-12 11:19:22 -04:00
BzKevin
5d3910da49 Tab to Space 2019-03-05 09:14:28 -05:00
BzKevin
f740d34f1f Tab to Space 2019-03-05 09:12:54 -05:00
BzKevin
58ca70c511 Tab to Space 2019-03-05 09:11:28 -05:00
BzKevin
ee59540648 Refactored
I was having issue wit this script periodically. The original version of this was real a hack-job of Holger Klemms original. This new version utilizes tables in a manner that I hope will make the code easier to follow and understand, but also allows for certain functions to be re-used throughout (and potentially in different scripts as well). In general:

Each program (align image stack, enfuse, exiftool) how has a table that contains information about that program, this includes a sub-table that contains argument data for the program.
There is a GUI table which contains all gui elements. Those elements are split into sub-tables within GUI based on what aspect of the scrpt behavior they relate to (AIS, ENF, Target File, Presets, etc...)
These two structures (and the similarities between them) have allows me to create loops which populate the arguments from the GUI and vice-versa, this is all done through an intermediary preference, the "active" preference. 
Whenever a change to a GUI element occurs the associated member of hte active preference is updated. then when it becomes time to build an argument string for a program command the active preference is read from and the value contained within is used. This works fine for all GUI elements that have a "changed" or "clicked" callback, as they can remain up-to-date real time. Sliders and Entry boxes do not have this, so at a certain point withing the script the GUI values are read directly for these elements and written to the associated active preference field.

As you will see about 50% of this code is simply building all the GUI elements and making sure that the initialization value for that element is valid (via the InRange() function). The actual heart of the program and it's functions really only takes up about 300 lines (starting at 180 and going through 476). That is broken down into multiple fairly concise functions. Hopefully this makes maintenance, feature enhancement, and troubleshooting much easier in the future.

I also made a few UI tweaks. This script use to create such a massive list of GUI elements that it was a nuisance when actually using it. They are now grouped into three containers for AIS options, Enfuse options, and target options. Only one of these options containers is displayed at a time. Additionally, I removed all the registered preferences I had before, so now I only register 3 options to enable user to point towards the program binaries.
2019-03-04 13:13:49 -05:00
BzKevin
9e97b8f5a1 Added comments, made the gap inclusive
Added comments throughout
Change the gap comparison to be inclusive ( from '<' to '<=')
2019-03-04 12:47:52 -05:00
BzKevin
95968c3d95 Add AutoGrouper
Adds AutoGrouper script which allows the user to select multiple images (or an entire collection) and automatically group them based on timestamp. The separation between groups is user defined via a slider.
2019-03-04 12:20:55 -05:00
BzKevin
85c242ff0e Comments added, minor updates
I have added comments throughout, mainly to the functions
I improved the performance of the CleanSpaces() function
Improved communication to user via dt.print()
2019-03-04 09:45:05 -05:00
supertobi
18783e7ea5 kml_export fixed typo 2019-03-04 15:12:04 +01:00
BzKevin
7bc4846c0a Refactored
After experiencing some issues after upgrading to dt2.6 I decided it would be worthwhile to refactor the code for more reliable performance. Specifically the script would crash dt when attempting to apply a style on import and sometimes just fail to run. I have refactored this in a manor that should make it more logical for future users to add/maintain as applicable. 

Refactored script has only been tested on Windows 10 w/ dt2.6. Hopefullt all cross-platform aspects have been maintained.
2019-03-03 15:48:08 -05:00
supertobi
49c4406bb4 Update video_mencoder.lua
fix linebrakes
2019-03-03 17:41:04 +01:00
supertobi
fedf2229a7 Merge pull request #176 from darktable-org/supertobi-patch-OpenInExplorer
OpenInExplorer
2019-03-01 23:32:53 +01:00
supertobi
2e5e2e9f0d OpenInExplorer added Nautilus support 2019-03-01 23:30:58 +01:00
supertobi
fa25ec4a26 OpenInExplorer Tap to Space 2019-03-01 23:27:26 +01:00
supertobi
0ef9f5fc6c kml_export df.mkdir(df.sanitize_filename())
df.mkdir(df.sanitize_filename(xportDirectory..PS..imageFoldername))
2019-03-01 14:22:35 +01:00
supertobi
3df9040cfb Merge pull request #175 from BzKevin/patch-2
gettext incorrectly pointed to "gimp"
2019-02-28 22:21:36 +01:00
BzKevin
bd3a74954c gettext incorrectly pointed to "gimp" 2019-02-28 15:08:57 -05:00
supertobi
69b9a1fa5c Merge pull request #173 from darktable-org/supertobi-patch-video_ffmpeg
video_ffmpeg add df.sanitize_filename()
2019-02-28 13:23:30 +01:00
supertobi
287d9e94cf video_ffmpeg
I forgot the argument
2019-02-28 13:22:10 +01:00
supertobi
24862a26f2 viedeo_ffmpeg df.sanitize_filename()
viedeo_ffmpeg df.sanitize_filename() for mkdir and rmdir
2019-02-28 11:41:56 +01:00
wpferguson
a319428489 Merge pull request #174 from wpferguson/fix_mkdir_rmdir_sanitize
make rmdir and mkdir path handling consistent with other dtutils.file routines
2019-02-27 13:31:47 -05:00
Bill Ferguson
11881b3b73 removed sanitize_filename wrapper from path argument 2019-02-27 13:29:29 -05:00
supertobi
5a6ee174af kml_export sanitize_filename()
dsys.launch_default_app(df.sanitize_filename(path))
2019-02-27 10:05:59 +01:00
supertobi
9183beeed4 video_ffmpeg add df.sanitize_filename()
I'm not 100% sure, but I think we need the df.sanitize_filename() here.
2019-02-27 09:57:36 +01:00
wpferguson
e5537fdff0 Merge pull request #162 from BzKevin/BzKevin-CollectHelper
Add CollectHelper
2019-02-26 17:02:13 -05:00
BzKevin
9e0490d371 Add gettext, check for color label
Add translate-ability. Also adds a check that the selected image has an active color label and displays a message if it does not
2019-02-26 14:30:11 -05:00
wpferguson
2b2a09e36d Merge pull request #172 from wpferguson/fix_open_default_app
fix lib/dtutils/system/open_default_app()
2019-02-26 12:06:21 -05:00
Bill Ferguson
5ff36fe2ec Added a space after the commands and removed quotes in the command invocation. 2019-02-26 12:03:04 -05:00
supertobi
9440085169 kml_export: use more utility functions
kml_export: use more utility functions as suggested by @wpferguson
2019-02-26 14:02:16 +01:00
supertobi
5c24077a30 kml_export
the command "convert" is deprecated and replaced by "magick"
2019-02-25 17:06:34 +01:00
supertobi
68a8eabbbb kml_export added preference for path to convert
kml_export added preference for path to convert as windows programs are not by default in the PATH
2019-02-25 16:31:22 +01:00
supertobi
a038333cce make kml_export multi OS
replaced mkdir, xdg-open, xdg-user-dir
2019-02-25 10:37:16 +01:00
supertobi
a5304fe74c First step to make kml_export.lua windows friendly
Replaced "/" with PS like in other scripts
2019-02-25 09:48:50 +01:00
wpferguson
aaf564b35c Merge pull request #168 from wpferguson/compatibility_updates
Compatibility updates
2019-02-24 17:28:56 -05:00
Bill Ferguson
de087f743f Fixed compatibility issues. Added new version check to scripts. Renamed lib/dtutils/file/rm() to lib/dtutils/file/rmdir since removing a file in windows with rmdir /q/s results in an error. Fixed os.tmpname() issued in enfuse. Under windows os.tmpname() doesn't put the file in a temporary directory, so fixed it to use darktable.configuration.tmp_dir. Cleaned up documentation in lib/dtutils/system.lua. 2019-02-24 15:19:37 -05:00
supertobi
21a8e441d2 Merge pull request #167 from wpferguson/deprecate_video_mencoder
deprecate video_mencoder
2019-02-22 09:42:06 +01:00
Bill Ferguson
32fc4556b1 Merge remote-tracking branch 'upstream/master' 2019-02-21 23:51:20 -05:00
wpferguson
238856ed89 Merge pull request #153 from cigno5/master
enfuse: convert raws to tiff automatically
2019-02-21 23:50:40 -05:00
Bill Ferguson
8f6a236eb6 Updated files 2019-02-21 23:49:52 -05:00
wpferguson
d9ce826b9d Merge pull request #157 from theres/dm_timelapse
Timelapse plugin based on ffmpeg
2019-02-21 11:57:03 -05:00
Bill Ferguson
76cc1d47f6 Added messages to video_mencoder.lua to notify users of it's deprecation and pending removal. 2019-02-21 11:53:56 -05:00
wpferguson
a3d0983d33 Merge pull request #166 from wpferguson/check_min_api_version
check_min_api_version
2019-02-20 11:50:36 -05:00
Bill Ferguson
245a7b59c4 Incorporated supertobi's comment about spacing. 2019-02-20 11:48:37 -05:00
Bill Ferguson
8b714edb67 Changed check_min_api_version() to print error messages and throw an error if the minimum api is not satisfied. Updated the scripts to reflect the changed check_min_api_version(). 2019-02-20 00:22:05 -05:00
Dominik Markiewicz
2a3d532881 video ffmpeg: fix default macos video location; replace missing path separator by const 2019-02-19 11:36:42 +01:00
Dominik Markiewicz
5555ee278e file lib: mkdir - handle existing directory case on Windows 2019-02-19 11:35:02 +01:00
Bill Ferguson
451c7fb956 Added check_min_api_function to dtutils.lua to check the minimum version of the lua API required for a script to run. This is intended to replace the API check done with darktable.configuration.check_version(), which requires changing each time the API version changes. check_min_api_version should work for all API version changes unless backward compatibility is broken. All the scripts using darktable.configuration.check_version() were changed to use check_min_api_version(). 2019-02-18 19:46:11 -05:00
wpferguson
391b3b9e4f Merge pull request #165 from BzKevin/patch-1
Update OpenInExplorer
2019-02-16 20:28:34 -05:00
BzKevin
130a8edcef Cancel script if not windows
added return to cancel installation of the script if not windows, after displaying a message to user
2019-02-15 20:23:07 -05:00
BzKevin
5fd3a56a31 Remove extraneous widgets in preferences
I now understand how preferences work better and have removed the extraneous widgets which were doing nothing
2019-02-15 14:24:51 -05:00
BzKevin
b682284a2a Update with 'local' variable designation
Updated variable definitions with 'local' designation. Doing so slightly broke the script so I modified the function calls a tad to pass variables in and out as needed for "CollectOnAll" function to work.
2019-02-15 14:08:04 -05:00
BzKevin
d1676ac2e0 Update OpenInExplorer
Added:
1) Check if no image selected or more than 15 images selected
2) Popup error message if not a windows OS detected
3) Fixed a typo
2019-02-15 13:40:03 -05:00
BzKevin
2cb3397d75 Update HDRMerge
Now only checks for binary proper install once the functionality of the program is actually utilized. This avoids to many external command calls at dt startup just to check if certain things are installed.
2019-02-15 13:22:49 -05:00
supertobi
936f4fe254 Merge pull request #163 from BzKevin/BzKevin-OpenInExplorer
Add OpenInExplorer
2019-02-14 09:45:59 +01:00
BzKevin
f611213662 Update OpenInExplorer
instead of being it's own lib this is now added to the selected image's module and the button text has been updated to match dt's style
2019-02-13 13:46:22 -05:00
BzKevin
ff0352cfe6 Updated version of Collect Helper
Reworded the button text to better match with the rest of darktable's buttons
2019-02-13 13:42:54 -05:00
BzKevin
4d6dfea3a5 Add CollectHelper 2019-02-13 12:23:34 -05:00
BzKevin
be386e1172 Add OpenInExplorer 2019-02-13 12:22:42 -05:00
BzKevin
2a2fbd18cd Add HDRMerge 2019-02-13 12:21:57 -05:00
theres
56159e91ce video_ffmpg: fix executable path widget for ffmpeg; try to get default location for files
- logic for craeting vidget for specifing ffmpeg location was invalid
- now default export location should be set to default system
Video/Movie directory
2019-02-04 08:41:54 +01:00
theres
3675f4d7cd video ffmpeg - post review fixes
- rename OS_PATH_SEPARATOR -> PS and use in all needed places
- change function name from `open_file` to `launch_default_app`
- move `mkdir`, `rm` and `launch_default_app` helper functions to libs
- add executable_path_widget to export module if needed, not to the
preferences
- add show_status
- remove support_format
- change function name `format` to `format_string`, add comments
- replace apostrophes by quote marks
2019-01-27 22:43:47 +01:00
theres
3a129d3c4f Timelapse plugin based on ffmpeg 2019-01-21 01:21:56 +01:00
wpferguson
cc4e7197ba Merge pull request #156 from wpferguson/readme_update
Update README.md
2019-01-14 23:56:00 -05:00
Bill Ferguson
fc5677e7b5 Added enfusedAdvanced and gen_i18n_mo descriptions 2019-01-14 23:53:26 -05:00
wpferguson
19542c1640 Merge pull request #148 from wpferguson/translation_tool
Translation tool
2019-01-14 23:33:52 -05:00
Bill Ferguson
68b5acc154 Updated comments to reflect name change 2019-01-12 22:27:06 -05:00
Bill Ferguson
5c2c9e2a8f Changed name to gen_18n_mo to further clarify what the program does. 2019-01-12 22:21:48 -05:00
wpferguson
0376e11544 Merge pull request #130 from BzKevin/master
Add enfuseAdvanced
2019-01-12 22:05:12 -05:00
supertobi
6d910ffb60 Merge pull request #154 from Hofer-Julian/patch-1
Typo
2019-01-11 15:17:25 +01:00
luca
32f6310947 Used DT internal conversion (in place of darktable-cli os call)
Now a TIFF exporter is instanced at the beginning and used for all
selected raws

No system call to darktable-cli anymore
2019-01-08 21:15:18 +01:00
luca
f464a52ea4 dt.print made properly with string.format
All print call have been replaced with string.format (when needed) and
call to dt.gettext, in a future opening to translation
2019-01-08 20:46:32 +01:00
Hofer-Julian
e3d755fb9a Typo 2019-01-07 13:10:06 +01:00
luca
e05e05d866 enfuse: convert raws to tiff automatically
it uses darktable-cli (if installed) to convert to a temporary file the
raw files that enfuse is not able to process.
2019-01-06 21:42:49 +01:00
wpferguson
b7ff1cd579 Merge pull request #151 from wpferguson/fix_script_manager_activate_script
Fix script manager script activation bug
2018-11-21 21:31:34 -05:00
Bill Ferguson
0550f4e28a Fixed bug where the wrong script could be activated, because the script to activate was found by string matching. Replaced string match with value of category selector and script name to avoid any ambiguity. 2018-11-21 21:24:28 -05:00
wpferguson
3456dc90d8 Merge pull request #150 from wpferguson/hugin_windows_fix
Hugin windows fix
2018-11-19 16:28:27 -05:00
Bill Ferguson
8e006cf853 Changed windows path quoting to double quotes instead of single. Changed dt.control.execute to dtsys.external_command to handle the windows quoted arguments. 2018-11-19 15:56:24 -05:00
supertobi
0e36af38d8 Merge pull request #146 from simon04/i18n-update
Update German translation of hugin and kml_export
2018-11-07 09:52:36 +01:00
supertobi
e10f3e2285 Merge pull request #149 from schwerdf/master
'LabelsToTags' is standalone.
2018-11-03 10:48:26 +01:00
August Schwerdfeger
31f1614679 'LabelsToTags' is standalone. 2018-11-02 20:47:09 -05:00
Bill Ferguson
80e7eca064 Added tool to compile source translation files and place the results in the correct locale directory 2018-09-26 23:59:42 -04:00
BzKevin
adb85e349a Update enfuseAdvanced.lua
Fixing accidental "invalid escape" at line 133
2018-09-18 11:54:24 -04:00
BzKevin
52a241c1ec Delete enfuse_pro_2
enfuse_pro_2 is now enfuseAdvanced
2018-09-18 10:55:23 -04:00
BzKevin
d092016729 Add enfuseAdvanced
enfuseAdvanced replaces what was previously known as enfuse_pro_2. This incorporates changes requested by wpferguson on previous versions.
2018-09-18 10:54:16 -04:00
Simon Legner
11607e7e7d Delete compiled .mo files 2018-09-18 10:26:40 +02:00
Simon Legner
2d01f801bc Update German translation of hugin and kml_export 2018-09-18 10:25:34 +02:00
wpferguson
6e15373ce9 Merge pull request #145 from wpferguson/rename_tags_speed_up
rename-tags speed up
2018-09-13 10:55:04 -04:00
Bill Ferguson
e95ed5ad2b Found a way to access only the images tagged with the old tag instead of searching through all the images in the database. This results in a huge decrease in execution speed, depending on the size of the database. 2018-09-12 16:41:42 -04:00
wpferguson
dc9324f322 Merge pull request #144 from wpferguson/script_manager_dash_bug
script manager dash bug
2018-08-26 00:44:03 -04:00
Bill Ferguson
e7adf8cfb2 string.match on a filename with a - resulted in a failed match. Added a check for the dash and escaped it. 2018-08-26 00:39:58 -04:00
wpferguson
5f7c49ca85 Merge pull request #143 from wpferguson/script_manager_win_bug
fixed buy on windows where preferences were not getting saved
2018-08-23 13:10:58 -04:00
Bill Ferguson
f724335b91 fixed buy on windows where preferences wee not getting saved 2018-08-23 12:50:50 -04:00
wpferguson
3a5dfbc842 Merge pull request #135 from wpferguson/image_stack
Image Stacking
2018-08-23 11:45:16 -04:00
Bill Ferguson
380a1b66e6 Added path separator constant and used it where necessary. Added comment to attribute copy 2018-08-22 19:33:11 -04:00
wpferguson
c6cccf2d93 Merge pull request #141 from wpferguson/revert_script_manager_and
reverted change from nested if statements to a multiple and statement
2018-08-20 23:50:36 -04:00
Bill Ferguson
0858a19138 reverted change from nested if statements to a multiple and statement because it didn't work 2018-08-20 23:47:30 -04:00
wpferguson
f42199aec0 Merge pull request #140 from wpferguson/script_manager_typo
fixed typo in preferences
2018-08-20 23:28:04 -04:00
Bill Ferguson
8d746427a6 fixed typo in preferences 2018-08-20 23:24:30 -04:00
wpferguson
3c9b2b90d4 Merge pull request #139 from wpferguson/script_manager
darktable script management interface
2018-08-20 21:29:32 -04:00
Bill Ferguson
abc959a814 enabled link to lua-scripts version 2018-08-20 21:28:54 -04:00
wpferguson
c71c289547 Merge pull request #138 from wpferguson/executable_manager
Manage external executables used by the lua scripts
2018-08-20 21:22:47 -04:00
Bill Ferguson
02bb942abd Cleaned up the code and incorporated Tobias Jakob's comments 2018-08-20 21:12:07 -04:00
Bill Ferguson
a37c33ec36 Fixed enable preference not saved 2018-08-19 12:30:45 -04:00
Bill Ferguson
0e4926426f Added error feedback when script doesn't load 2018-08-19 01:11:54 -04:00
Bill Ferguson
a309db8739 Created tools/script_manager.lua to provide an interface from darktable for managing the lua scripts. Updated README.md to reflect the added script. 2018-08-18 15:04:43 -04:00
Bill Ferguson
1a998581ef Created tools/executable_manager.lua for managing external executables used by the lua scripts. Updated the README to reflect the new script. 2018-08-17 19:30:43 -04:00
wpferguson
f49b951331 Merge pull request #137 from wpferguson/enfuse_version_aware
Alter enfuse arguments based on version
2018-08-15 11:25:05 -04:00
Bill Ferguson
26e16ffeda Added a version check to modify the arguments based on the version of enfuse. Enfuse 4.2 no longer accepts the exposure-mu argument. The closest function is exposure-optimum. For versions of enfuse less than 4.2 exposure-mu is used. For versions of enfuse greater than 4.2, exposure-optimum is used. 2018-08-15 00:30:18 -04:00
Bill Ferguson
9e4a72e098 Creeated image_stack.lua based on Pat David's blog entry, https://patdavid.net/2013/05/noise-removal-in-photos-with-median_6.html. Updated README.md to reflect the new script. 2018-08-08 22:41:25 -04:00
wpferguson
a7d053648a Merge pull request #134 from wpferguson/multi_os_example
Multiple OS Example
2018-08-08 21:24:23 -04:00
Bill Ferguson
61a55f71f5 Created a cross platform script that calls an external executable. It also performs a service that some users might find useful, extracting the embedded jpeg image from a raw file, then importing and grouping it with the raw. Updated README.md to reflect the new file 2018-08-07 22:08:09 -04:00
wpferguson
9979d4abda Merge pull request #132 from wpferguson/windows_work_arounds
Cross platform external command capability
2018-08-07 11:02:04 -04:00
Bill Ferguson
5a2af47eda Removed unneeded require for yield, since it now in lib/dtutils.system 2018-08-07 11:00:16 -04:00
wpferguson
eab6b99155 Merge pull request #133 from sjjh/patch-1
corrected "dt" to "darktable" in line 63 of cr2hdr
2018-08-03 22:28:31 -04:00
sjjh
4acba4d48a corrected "dt" to "darktable" in line 63 of cr2hdr
object "darktable" is defined in line 35; usage of "dt" was probably a typo or left over from a refactoring.
2018-08-04 01:43:38 +02:00
Bill Ferguson
8b5a43e435 Added require statement for official/yield.lua so that darktable.control.execute() would have all of it's requirements met without the user having to remember to add it somewhere else 2018-07-12 13:23:33 -04:00
Bill Ferguson
26fce03d0f Removed leftover unused code 2018-07-11 11:49:01 -04:00
Bill Ferguson
1a22bfbcef Added system library to handle running external commands on different operating systems in a transparent manner. Added file and path quoting that is cross platform compatible. Changed gimp.lua to use the new utilities. 2018-07-11 11:32:48 -04:00
BzKevin
050783fc8b enfuse_pro_2 modified per wpferguson
1) Moved check_if_bin installed to run only once, when script first loaded (rather than every time it is called)
2) Changed executable path preferences per Bill's suggestions 
   dt.preferences.register("executable_paths", "align_image_stack",
   dt.preferences.register("executable_paths", "enfuse",
   dt.preferences.register("executable_paths", "exiftool",
3) Replaced custom block of code with df.create_unique_filename
4) Quoted paths in run commands for linux and MacOS (I still experience failures if I quote anything in the run command aside from the .exe path on windows)
5) Changed LUA API compatability check for 5.0.0 only

Tested on Windows and MacOS
2018-07-07 11:40:01 -04:00
wpferguson
d96d8fb4fc Merge pull request #131 from wpferguson/macos_command_fixes
MacOS Command Fixes
2018-07-07 11:08:23 -04:00
Bill Ferguson
4b32c34ebd Removed 'open -W -a' for mac commands from check_if_bin_exists since it only appears to be needed for GUIs. Added 'open -W -a' for macs using GIMP in gimp.lua 2018-07-06 23:04:44 -04:00
BzKevin
927bf12243 Add enfuse_pro_2
Enhanced version of Holger Klemm's enfuse_pro that adds features and X-platform compatibility.
2018-07-01 19:54:15 -04:00
BzKevin
a867791165 Delete enfuse_pro_2.lua 2018-07-01 19:52:47 -04:00
BzKevin
67232094c9 Delete OpenInPS.lua 2018-07-01 19:51:39 -04:00
BzKevin
b5e79abf08 Delete HDRMerge.lua 2018-07-01 19:51:26 -04:00
BzKevin
84c0fd7414 Add 3 new Pulg-Ins
HDRMerge: Add easy access to HDRMerge from within dt
enfuse_pro_2: Updated version of Holger Klemm's popular enfuse plug-in. Adds some features and cross-platform compatability
OpenInPS: add ability to open or export selected file with Photoshop
2018-06-30 14:58:50 -04:00
supertobi
850a7d8676 Merge pull request #126 from jbvoelker/gpx_tracks
gpx_export.lua: Create a separate <trk> for each source folder
2018-06-24 22:37:12 +02:00
Jannis_V
ded24e8037 gpx_export.lua: Create a separate <trk> for each source folder 2018-06-24 18:47:33 +02:00
Tobias Ellinghaus
71fbf5705c Use print_log in import_filter_manager 2018-05-23 10:23:48 +02:00
Tobias Ellinghaus
87b78717f3 Fix formatting of README.md 2018-05-23 10:22:37 +02:00
wpferguson
1cd9a34761 Merge pull request #123 from wpferguson/README.md_snap_update
Add snap information to README.md
2018-05-22 12:13:25 -04:00
Bill Ferguson
b9072cccba Updated README.md to reflect change to snap packaging enabling lua and included instructions on how to use the scripts with the snap 2018-05-22 12:11:55 -04:00
Tobias Ellinghaus
18fb3a68fd Re-fix README.md 2018-05-21 11:23:19 +02:00
wpferguson
c0b89e51d7 Merge pull request #122 from wpferguson/update_README.md_for_snaps
Updated README.md to include snap, flatpak, and appimage information
2018-05-20 15:10:32 -04:00
wpferguson
f2ac2595f9 Merge branch 'master' into update_README.md_for_snaps 2018-05-20 15:09:35 -04:00
Bill Ferguson
b241007d4f Updated README.md to include snap, flatpak, and appimage information 2018-05-20 15:02:37 -04:00
Tobias Ellinghaus
2b1af40bd1 Some fixes for the README 2018-05-20 16:11:02 +02:00
wpferguson
66f85a1f35 Merge pull request #121 from wpferguson/update_README.md
Update README.md
2018-05-19 23:17:15 -04:00
Bill Ferguson
22633d6130 Updated README.md to better present what the lua-scripts are and how to use them. Provided tables of scripts, whether they are standalone, and cross platform compatibility. Provided instructions for install on Linux, MacOS, and Windows as well as instructions for updating the scripts. 2018-05-18 22:28:33 -04:00
wpferguson
8ed4d838ee Merge pull request #120 from wpferguson/hugin_space_problem
hugin fails on images with spaces in the filenames
2018-05-16 11:15:37 -04:00
wpferguson
9175916771 Merge pull request #119 from wpferguson/fix_print_log_plaeceholder
Replace placeholder for dt.print_log with dt.print_log
2018-05-15 11:38:56 -04:00
Bill Ferguson
7acf2b9cac Fixed bug where log_level() always returned log.success instead of the correct log level 2018-05-15 01:22:09 -04:00
Bill Ferguson
965d8791d8 single quoted filename arguments to the hugin commands for the supplied image files. Single quoted any filenames derived from the supplied image files in case they contained spaces. Added logging at various levels so that the next time I have to work on this I can figure it out quicker. Normal logging level is log.error which only prints error information. Changing log level to log.info provides much more insight about what is occurring. Fixed gui not returning generated file to film strip. 2018-05-15 01:05:34 -04:00
Bill Ferguson
f961e6aa6b Replaced dt_print_log = dt.print_error with dt.print_log now that the function exists 2018-05-14 20:30:09 -04:00
wpferguson
b8182cfde5 Merge pull request #117 from marco44/fix_autostyle
Fix autostyle
2018-05-09 14:48:55 -04:00
marco44
78f0e367dc Change logging 2018-05-09 17:54:26 +02:00
marco44
aca0224263 Fix autostyle, checkIfBinExists doesn't seem to exist in latest releases anymore 2018-05-05 16:37:27 +02:00
supertobi
c74636e2c0 Fixed typo in README 2018-04-09 16:54:12 +02:00
supertobi
f113bc5b8a Added links to the README
Added links to the documentation.
2018-04-09 13:23:11 +02:00
supertobi
befae8b191 Improved README.md
I've added some information anarcat suggested on pixls.us:
https://discuss.pixls.us/t/don-t-forget-the-lua-scripts/1598/8
2018-04-09 11:59:52 +02:00
supertobi
1767e8f074 Merge pull request #111 from achienbsi/hugin_plugin_improvements
hugin plugin improvements
2018-03-19 22:32:19 +01:00
Andy Chien
44081b772f fixed version requirement due to updates to darktable.configuration.running_os 2018-03-19 06:51:30 -07:00
supertobi
3051eddd2e Merge pull request #115 from wpferguson/dtutils_file_fix
check_if_[bin|file]_exist fixes
2018-03-17 10:24:24 +01:00
Bill Ferguson
5266aa7547 Used a less aggressive quoting strategy that works with check_if_bin_exists, check_if_file_exists, and io.popen(). 2018-03-16 12:16:45 -04:00
supertobi
7e57f61947 Merge pull request #114 from wpferguson/check_if_bin_exists_fixes
Fix macOS returned path  and add wine awareness to check_if_bin_exists()
2018-03-11 16:49:53 +01:00
Bill Ferguson
1c587d3726 Changed wine test to check for .exe"$ or .EXE"$ because of quoting string paths 2018-03-10 13:45:04 -05:00
Bill Ferguson
b4ea97ce37 Fixed accidently wiped out check for macos 2018-03-10 13:38:17 -05:00
Bill Ferguson
83c1a5789a Changed wine check to operating system isn't windows 2018-03-10 13:07:19 -05:00
Bill Ferguson
841026661a Removed debugging statement 2018-03-10 12:56:18 -05:00
Bill Ferguson
692441e1d5 Added wine awareness for unix, linux, and macos. Added open -a -W to the macos path so that the path returned by the finder will work correctly. 2018-03-10 12:52:22 -05:00
Andy Chien
46ba555a20 * updated to work with new dtutils_file
* added checkbox to allow user to launch hugin in gui mode
* changed the logic so hugin will write temp files to tmp directory and then move the final result to the directory of original source files. this improves speed when original source files are on network mapped drive
* added some additional error messages
2018-03-05 09:48:30 -08:00
supertobi
de18432a7d Merge pull request #112 from wpferguson/os_path_fix
Windows and macOS external executable compatibility
2018-03-05 14:13:29 +01:00
supertobi
497b6cb59c Merge pull request #113 from wpferguson/check_os
Check operating system compatability
2018-03-04 22:27:08 +01:00
Bill Ferguson
c073ecf0dd Added check_os to lib/dtutils.lua so that scripts can check compatibility with the current operating system 2018-03-03 20:22:47 -05:00
Bill Ferguson
2ea4db8e38 Added windows and macOS external executable compatibility. Added functions set_executable_path_preference(), get_executable_path_preference(), and executable_path_widget() to lib/dtutils/file.lua to get, set and retrieve the location of executables on windows and macOS systems. Modified check_if_bin_exists to use the path preferences so executables can be found on windows and macOS systems as well as linux. Modified contrib/gimp.lua to use the new functions, thus making it usable on windows and macOS too. 2018-03-02 14:07:29 -05:00
Andy Chien
fe942100a4 hugin plugin improvements
* added preference requiring user to specify the paths for hugin, hugin_executor and pto_gen needed for this plguin
* added preference to allow user to choose to launch hugin in GUI mode
* minor fix to the way files are handled; due to some versions of hugin_executor not writing out to the path specified in --prefix, assume we are working in the tmp directory and then move the file afterwards
2018-02-26 07:29:41 -08:00
supertobi
79eb1428b0 Merge pull request #108 from achienbsi/file_fixes_for_windows
fixed a couple dtutils_file functions for windows platform
2018-02-26 15:57:11 +01:00
Andy Chien
467365dcbb platform dependent implementation of check_if_bin_exists and check_if_file_exists 2018-02-23 18:14:24 -08:00
supertobi
33a323fe85 Merge pull request #109 from achienbsi/hugin_improvements
improved a few aspects of the hugin script
2018-02-23 09:41:32 +01:00
Andy Chien
4ad9e77a7e improved a few aspects of the hugin script:
* handle stiching all from hugin_executor without the gui
* automatically create the panorama file name based on first selected file plus _pano
* automatically detects if output file exists, if so use an unique name
* delete temporarily exported file
2018-02-22 16:07:28 -08:00
Andy Chien
5d57347730 fixed a couple dtutils_file functions for windows platform 2018-02-22 16:01:54 -08:00
Tobias Ellinghaus
54572de8d9 Fix LabelsToTags for Lua 5.3
Fixes #106
2018-01-23 11:28:45 +01:00
supertobi
0e57a0804a Merge pull request #105 from elman22/master
Updated KML/KMZ export script
2018-01-20 14:27:04 +01:00
Tobias Ellinghaus
95077f639b New script to delete unused tags on startup 2018-01-17 10:42:44 +01:00
Tobias Ellinghaus
493dcc7ef7 Don't delete tags while iterating 2018-01-17 10:41:55 +01:00
Elman
f86b16735b Formatted code to follow project settings 2018-01-15 22:54:22 +01:00
Elman
0c1de20a1a Code formatting 2018-01-15 17:56:02 +01:00
Elman
e9d0f706ed Correct handling of duplicates 2018-01-15 17:41:26 +01:00
Elman
d297718358 Path is now sorted by exif_datetime_taken 2018-01-15 17:40:50 +01:00
Elman
b406b36cda Added all Slovak and Czech accents 2018-01-15 17:31:19 +01:00
supertobi
570732a9b2 Merge pull request #104 from schwerdf/master
New script: "Labels to Tags"
2018-01-08 11:33:25 +01:00
August Schwerdfeger
92b5507e0e Added 'Labels to Tags' script. 2018-01-07 00:37:41 -06:00
supertobi
73897b8ee7 Merge pull request #103 from jbvoelker/gpx-export
GPX export script
2018-01-03 22:27:46 +01:00
Jannis Völker
2d9cdc5a24 GPX export script which generates a GPX track from all selected images containing geo information 2018-01-03 20:51:35 +01:00
supertobi
234e5bc5fc Merge pull request #102 from wpferguson/api_5_0_0
Updated remaining scripts to api 5.0.0
2018-01-03 10:08:22 +01:00
Bill Ferguson
664496b430 Updated to api 5.0.0 and tested. include_all.lua changed to load official/yield and ignore the lib and tools directories. 2018-01-02 19:51:14 -05:00
supertobi
67d92bd070 Update selection_to_pdf.lua
added {5,0,0} to the check_version
2018-01-02 11:37:10 +01:00
supertobi
d6e53cc005 Update save_selection.lua
added {5,0,0} to the check_version
2018-01-02 11:35:57 +01:00
supertobi
2c77712ab4 Update image_path_in_ui.lua
added {5,0,0} to the check_version
2018-01-02 11:34:41 +01:00
supertobi
10c1eff1b6 Update generate_image_txt.lua
added {5,0,0} to the check_version
2018-01-02 11:33:40 +01:00
supertobi
4c26465dde Update enfuse.lua
added {5,0,0} to the check_version
2018-01-02 11:30:08 +01:00
supertobi
d29e5f49f4 Update delete_long_tags.lua
added {5,0,0} to the check_version
2018-01-02 11:28:44 +01:00
supertobi
0191558d26 Update copy_paste_metadata.lua
added {5,0,0} to the check_version
2018-01-02 11:24:42 +01:00
supertobi
d3692bcc09 check_for_updates.lua: added {5,0,0} to the check_version 2018-01-02 11:22:43 +01:00
supertobi
befa83bee8 added {5,0,0} to the check_version 2018-01-02 11:10:56 +01:00
supertobi
b4347e5701 added {5,0,0} to the check_version 2018-01-02 11:10:31 +01:00
supertobi
c4ed6c2b97 added {5,0,0} to the check_version 2018-01-02 11:09:42 +01:00
supertobi
6b5f650aac added {5,0,0} to the check_version 2018-01-02 11:08:49 +01:00
supertobi
63b38c1f53 added {5,0,0} to the check_version 2018-01-02 11:08:07 +01:00
supertobi
33b6f0d72e added {5,0,0} to the check_version 2018-01-02 11:07:20 +01:00
supertobi
a6f8070f65 Delete LICENSE 2017-12-12 16:36:38 +01:00
supertobi
29aaaf55cf Merge pull request #101 from darktable-org/add-license-1
Create LICENSE
2017-12-12 15:19:01 +01:00
supertobi
b47da137c1 Create LICENSE 2017-12-12 15:18:40 +01:00
GHswitt
8e6552ee9c Added ignore tag and unknown tag, verbose output 2017-12-11 17:43:53 +01:00
GHswitt
024e016df2 Storage plugin for face_recognition 2017-12-11 14:27:36 +01:00
69 changed files with 8946 additions and 979 deletions

215
README.md
View File

@@ -1,10 +1,215 @@
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.
### 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|Yes|LMW|Check for updates to darktable
copy_paste_metadata|Yes|LMW|Copy and paste metadata, tags, ratings, and color labels between images
delete_long_tags|Yes|LMW|Delete all tags longer than a specified length
delete_unused_tags|Yes|LMW|Delete tags that have no associated images
enfuse|No|L|Exposure blend several images (HDR)
generate_image_txt|No|L|Generate txt sidecar files to be overlaid on zoomed images
image_path_in_ui|Yes|LMW|Plugin to display selected image path
import_filter_manager|Yes|LMW|Manager for import filters
import_filters|No|LMW|Two import filters for use with import_filter_manager
save_selection|Yes|LMW|Provide save and restore from multiple selection buffers
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|Yes|LMW|Group images together by time
autostyle|Yes|LMW|Automatically apply styles on import
clear_GPS|Yes|LMW|Reset GPS information for selected images
CollectHelper|Yes|LMW|Add buttons to selected images module to manipulate the collection
copy_attach_detach_tags|Yes|LMW|Copy and paste tags from/to images
cr2hdr|Yes|L|Process image created with Magic Lantern Dual ISO
enfuseAdvanced|No|LMW|Merge multiple images into Dynamic Range Increase (DRI) or Depth From Focus (DFF) images
ext_editor|No|LW|Export pictures to collection and edit them with up to nine user-defined external editors
face_recognition|No|LM|Identify and tag images using facial recognition
fujifilm_ratings|No|LM|Support importing Fujifilm ratings
geoJSON_export|No|L|Create a geo JSON script with thumbnails for use in ...
geoToolbox|No|LMW|A toolbox of geo functions
gimp|No|LMW|Open an image in GIMP for editing and return the result
gpx_export|No|LMW|Export a GPX track file from selected images GPS data
HDRMerge|No|LMW|Combind the selected images into an HDR DNG and return the result
hugin|No|LMW|Combine selected images into a panorama and return the result
image_stack|No|LMW|Combine a stack of images to remove noise or transient objects
kml_export|No|L|Export photos with a KML file for usage in Google Earth
LabelsToTags|Yes|LMW|Apply tags based on color labels and ratings
OpenInExplorer|No|LMW|Open the selected images in the system file manager
passport_guide|Yes|LMW|Add passport cropping guide to darkroom crop tool
pdf_slideshow|No|LM|Export images to a PDF slideshow
quicktag|Yes|LMW|Create shortcuts for quickly applying tags
rate_group|Yes|LMW|Apply or remove a star rating from grouped images
rename-tags|Yes|LMW|Change a tag name
RL_out_sharp|No|LW|Output sharpening using GMic (Richardson-Lucy algorithm)
select_untagged|Yes|LMW|Enable selection of untagged images
slideshowMusic|No|L|Play music during a slideshow
transfer_hierarchy|Yes|LMW|Image move/copy preserving directory hierarchy
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|Yes|LMW|Print the current API version
darkroom_demo|Yes|LMW|Demonstrate changing images in darkoom
gettextExample|Yes|LM|How to use translation
hello_world|Yes|LMW|Prints hello world when darktable starts
lighttable_demo|Yes|LMW|Demonstrate controlling lighttable mode, zoom, sorting and filtering
moduleExample|Yes|LMW|How to create a lighttable module
multi_os|No|LMW|How to create a cross platform script that calls an external executable
panels_demo|Yes|LMW|Demonstrate hiding and showing darktable panels
preferenceExamples|Yes|LMW|How to use preferences in a script
printExamples|Yes|LMW|How to use various print functions from a script
running_os|Yes|LMW|Print out the running operating system
### 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|Yes|LMW|Manage the external executables used by the lua scripts
gen_i18n_mo|No|LMW|Generate compiled translation files (.mo) from source files (.po)
get_lib_manpages|No|LM|Retrieve the library documentation and output it in man page and PDF format
get_libdoc|No|LMW|Retrieve the library documentation and output it as text
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)
## 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. It is currently in the edge channel, but should reach the stable channel soon.
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 and 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:
Open a command prompt.
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 "<directory>/<name>"` where directory is the directory containing the scripts, from the above list, and name is the name from the above list. To include GIMP the line would be `require "contrib/gimp"`.
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
### Linux and MacOS
cd ~/.config/darktable/lua/
git pull
### Windows
cd %LOCALAPPDATA%\darktable\lua
git pull
## Documentation
Each script includes its own documentation and usage in its header, please refer to them.
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.
Lua-script libraries documentation may be generated using the tools in the tools/ directory.
More information about the scripting with Lua can be found in the darktable user manual:
https://www.darktable.org/usermanual/en/lua_chapter.html
The darktable Lua API documentation is here:
https://www.darktable.org/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 and MacOS
Open a terminal and start darktable with the command `darktable -d lua`. This provides debugging information to give you insight into what is happening.
### Windows
Open the Git Bash Shell. Start darktable with the command `/c/Program\ Files/darktable/bin/darktable -d lua`. 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).

158
contrib/AutoGrouper.lua Normal file
View File

@@ -0,0 +1,158 @@
--[[AutoGrouper plugin for darktable
copyright (c) 2019 Kevin Ertel
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[About this Plugin
This plugin adds the module "Auto Group" to darktable's lighttable view
----REQUIRED SOFTWARE----
None
----USAGE----
Install: (see here for more detail: https://github.com/darktable-org/lua-scripts )
1) Copy this file in to your "lua/contrib" folder where all other scripts reside.
2) Require this file in your luarc file, as with any other dt plug-in
Set a gap amount in second which will be used to determine when images should no
longer be added to a group. If an image is more then the specified amount of time
from the last image in the group it will not be added. Images without timestamps
in exif data will be ignored.
There are two buttons. One allows the grouping to be performed only on the currently
selected images, the other button performs grouping on the entire active collection
]]
local dt = require "darktable"
local MOD = 'autogrouper'
local gettext = dt.gettext
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("AutoGrouper",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("AutoGrouper", msgid)
end
local function InRange(test, low, high) --tests if test value is within range of low and high (inclusive)
if test >= low and test <= high then
return true
else
return false
end
end
local function CompTime(first, second) --compares the timestamps and returns true if first was taken before second
first_time = first.exif_datetime_taken
if string.match(first_time, '[0-9]') == nil then first_time = '9999:99:99 99:99:99' end
first_time = tonumber(string.gsub(first_time, '[^0-9]*',''))
second_time = second.exif_datetime_taken
if string.match(second_time, '[0-9]') == nil then second_time = '9999:99:99 99:99:99' end
second_time = tonumber(string.gsub(second_time, '[^0-9]*',''))
return first_time < second_time
end
local function SeperateTime(str) --seperates the timestamp into individual components for used with OS.time operations
local cleaned = string.gsub(str, '[^%d]',':')
cleaned = string.gsub(cleaned, '::*',':') --YYYY:MM:DD:hh:mm:ss
local year = string.sub(cleaned,1,4)
local month = string.sub(cleaned,6,7)
local day = string.sub(cleaned,9,10)
local hour = string.sub(cleaned,12,13)
local min = string.sub(cleaned,15,16)
local sec = string.sub(cleaned,18,19)
return {year = year, month = month, day = day, hour = hour, min = min, sec = sec}
end
local function GetTimeDiff(curr_image, prev_image) --returns the time difference (in sec.) from current image and the previous image
local curr_time = SeperateTime(curr_image.exif_datetime_taken)
local prev_time = SeperateTime(prev_image.exif_datetime_taken)
return os.time(curr_time)-os.time(prev_time)
end
local function main(on_collection)
local images = {}
if on_collection then
local col_images = dt.collection
for i,image in ipairs(col_images) do --copy images to a standard table, table.sort barfs on type dt_lua_singleton_image_collection
table.insert(images,i,image)
end
else
images = dt.gui.selection()
end
dt.preferences.write(MOD, 'active_gap', 'integer', GUI.gap.value)
if #images < 2 then
dt.print('please select at least 2 images')
return
end
table.sort(images, function(first, second) return CompTime(first,second) end) --sort images by timestamp
for i, image in ipairs(images) do
if i == 1 then
prev_image = image
elseif string.match(image.exif_datetime_taken, '[%d]') ~= nil then --make sure current image has a timestamp, if so check if it is within the user specified gap value and add to group
local curr_image = image
if GetTimeDiff(curr_image, prev_image) <= GUI.gap.value then
images[i]:group_with(images[i-1])
end
prev_image = curr_image
end
end
end
-- GUI --
GUI = {
gap = {},
selected = {},
collection = {}
}
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
}
dt.register_lib(
'AutoGroup_Lib', -- 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
}
)

221
contrib/CollectHelper.lua Normal file
View File

@@ -0,0 +1,221 @@
--[[Collect Helper plugin for darktable
copyright (c) 2019 Kevin Ertel
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[About this plugin
This plugin adds the button(s) to the "Selected Images" module:
1) Return to Previous Collection
2) Collect on image's Folder
3) Collect on image's Color Label(s)
4) Collect on All (AND)
It also adds 3 preferences to the lua options dialog box which allow the user to activate/deactivate the 3 "Collect on" buttons.
Button Behavior:
1) Return to Previous Collection - Will reset the collect parameters to the previously active settings
2) Collect on image's Folder - Will change the collect parameters to be "Folder" with a value of the selected image's folder location
3) Collect on image's Color Label(s) - Will change the collect parameter to be "Color" with a value of the selected images color labels, will apply multiple parameters with AND logic if multiple exist
4) Collect on All (AND) - Will collect on all parameters activated by the preferences dialog, as such this button is redundant if you only have one of the two other options enabled
----REQUIRED SOFTWARE----
NA
----USAGE----
Install: (see here for more detail: https://github.com/darktable-org/lua-scripts )
1) Copy this file in to your "lua/contrib" folder where all other scripts reside.
2) Require this file in your luarc file, as with any other dt plug-in
Select the photo you wish to change you collection based on.
In the "Selected Images" module click on "Collect on this Image"
----KNOWN ISSUES----
]]
local dt = require "darktable"
local gettext = dt.gettext
local previous = nil
local all_active = false
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("CollectHelper",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("CollectHelper", msgid)
end
-- FUNCTION --
local function CheckSingleImage(selection)
if #selection ~= 1 then
dt.print(_("Please select a single image"))
return true
end
return false
end
local function CheckHasColorLabel(selection)
local ret = false
for _,image in pairs(selection) do
if image.red then ret = true end
if image.blue then ret = true end
if image.green then ret = true end
if image.yellow then ret = true end
if image.purple then ret = true end
end
return ret
end
local function PreviousCollection()
if previous ~= nil then
previous = dt.gui.libs.collect.filter(previous)
end
end
local function CollectOnFolder(all_rules, all_active)
local images = dt.gui.selection()
if CheckSingleImage(images) then
return
end
local rules = {}
local rule = dt.gui.libs.collect.new_rule()
rule.mode = "DT_LIB_COLLECT_MODE_AND"
rule.data = images[1].path
rule.item = "DT_COLLECTION_PROP_FOLDERS"
table.insert(rules, rule)
if all_active then
for _,active_rule in pairs(rules) do
table.insert(all_rules, active_rule)
end
return all_rules
else
previous = dt.gui.libs.collect.filter(rules)
end
end
local function CollectOnColors(all_rules, all_active)
local images = dt.gui.selection()
if CheckSingleImage(images) then
return
end
if not CheckHasColorLabel(images) then
dt.print(_('select an image with an active color label'))
return
end
for _,image in pairs(images) do
local rules = {}
if image.red then
local red_rule = dt.gui.libs.collect.new_rule()
red_rule.mode = "DT_LIB_COLLECT_MODE_AND"
red_rule.data = "red"
red_rule.item = "DT_COLLECTION_PROP_COLORLABEL"
table.insert(rules, red_rule)
end
if image.blue then
local blue_rule = dt.gui.libs.collect.new_rule()
blue_rule.mode = "DT_LIB_COLLECT_MODE_AND"
blue_rule.data = "blue"
blue_rule.item = "DT_COLLECTION_PROP_COLORLABEL"
table.insert(rules, blue_rule)
end
if image.green then
local green_rule = dt.gui.libs.collect.new_rule()
green_rule.mode = "DT_LIB_COLLECT_MODE_AND"
green_rule.data = "green"
green_rule.item = "DT_COLLECTION_PROP_COLORLABEL"
table.insert(rules, green_rule)
end
if image.yellow then
local yellow_rule = dt.gui.libs.collect.new_rule()
yellow_rule.mode = "DT_LIB_COLLECT_MODE_AND"
yellow_rule.data = "yellow"
yellow_rule.item = "DT_COLLECTION_PROP_COLORLABEL"
table.insert(rules, yellow_rule)
end
if image.purple then
local purple_rule = dt.gui.libs.collect.new_rule()
purple_rule.mode = "DT_LIB_COLLECT_MODE_AND"
purple_rule.data = "purple"
purple_rule.item = "DT_COLLECTION_PROP_COLORLABEL"
table.insert(rules, purple_rule)
end
if all_active then
for _,active_rule in pairs(rules) do
table.insert(all_rules, active_rule)
end
return all_rules
else
previous = dt.gui.libs.collect.filter(rules)
end
end
end
local function CollectOnAll_AND()
local images = dt.gui.selection()
if CheckSingleImage(images) then
return
end
local rules = {}
if dt.preferences.read('module_CollectHelper','folder','bool') then
rules = CollectOnFolder(rules, true)
end
if dt.preferences.read('module_CollectHelper','colors','bool') then
rules = CollectOnColors(rules, true)
end
previous = dt.gui.libs.collect.filter(rules)
end
-- GUI --
dt.gui.libs.image.register_action(
_("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(
_("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(
_("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(
_("collect: all (AND)"),
function() CollectOnAll_AND() end,
_("Sets the Collect parameters based on all activated CollectHelper options")
)
end
-- PREFERENCES --
dt.preferences.register("module_CollectHelper", "all_and", -- name
"bool", -- type
_('CollectHelper: All'), -- label
_('Will create a collect parameter set that utelizes 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
)

424
contrib/HDRMerge.lua Normal file
View File

@@ -0,0 +1,424 @@
--[[HDRMerge plugin for darktable
copyright (c) 2018 Kevin Ertel
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[About this Plugin
This plugin adds the module 'HDRMerge' to darktable's lighttable view
----REQUIRED SOFTWARE----
HDRMerge ver. 4.5 or greater
----USAGE----
Install: (see here for more detail: https://github.com/darktable-org/lua-scripts )
1) Copy this file in to your 'lua/contrib' folder where all other scripts reside.
2) Require this file in your luarc file, as with any other dt plug-in
On the initial startup go to darktable settings > lua options and set your executable paths and other preferences, then restart darktable
Select bracketed images and press the Run HDRMerge button. The resulting DNG will be auto-imported into darktable.
Additional tags or style can be applied on auto import as well, if you desire.
Base Options:
Select your desired BPS (bits per sample and Embedded Preview Size.
Batch Options:
Select if you want to run in batch mode or not
Select the gap, in seconds, between images for auto grouping in batch mode
See HDRMerge manual for further detail: http://jcelaya.github.io/hdrmerge/documentation/2014/07/11/user-manual.html
Auto-import Options:
Select a style, whether you want tags to be copied from the original, and any additional tags you desire added when the new image is auto-imported
]]
local dt = require 'darktable'
local df = require 'lib/dtutils.file'
local dsys = require 'lib/dtutils.system'
local mod = 'module_HDRMerge'
local os_path_seperator = '/'
if dt.configuration.running_os == 'windows' then os_path_seperator = '\\' end
-- Tell gettext where to find the .mo file translating messages for a particular domain
local gettext = dt.gettext
gettext.bindtextdomain('HDRMerge',dt.configuration.config_dir..'/lua/locale/')
local function _(msgid)
return gettext.dgettext('HDRMerge', msgid)
end
local temp
local HDRM = { --HDRMerge Program Table
name = 'HDRMerge',
bin = '',
first_run = true,
install_error = false,
arg_string = '',
images_string = '',
args = {
bps ={text = '-b ', style = 'integer'},
size ={text = '-p ', style = 'string'},
batch ={text = '-B ', style = 'bool'},
gap ={text = '-g ', style = 'integer'}
}
}
local GUI = { --GUI Elements Table
HDR = {
bps ={},
size ={},
batch ={},
gap ={}
},
Target = {
style ={},
copy_tags ={},
add_tags ={}
},
run = {},
stack = {},
options = {},
exes = {
HDRMerge = {},
update = {},
}
}
--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 locatoin'))
end
end
local function ExeUpdate(prog_tbl)
dt.preferences.write(mod, 'bin_exists', 'bool', true)
for _,prog in pairs(prog_tbl) do
dt.preferences.write('executable_paths', prog.name, 'string', GUI.exes[prog.name].value)
prog.bin = df.check_if_bin_exists(prog.name)
if not prog.bin then
prog.install_error = true
dt.preferences.write(mod, 'bin_exists', 'bool', false)
else
prog.bin = CleanSpaces(prog.bin)
end
prog.first_run = false
end
if dt.preferences.read(mod, 'bin_exists', 'bool') then
GUI.stack.active = 1
dt.print(_('update successful'))
else
dt.print(_('update unsuccessful, please try again'))
end
end
local function UpdateActivePreference() --sliders & entry boxes do not have a click/changed callback, so their values must be saved to the active preference
temp = GUI.HDR.gap.value
dt.preferences.write(mod, 'active_gap', 'integer', temp)
temp = GUI.Target.add_tags.text
dt.preferences.write(mod, 'active_add_tags', 'string', temp)
end
local function main()
PreCall({HDRM}) --check if furst run then check if install OK
if HDRM.install_error then
dt.print_error(_('HDRMerge install issue'))
dt.print(_('HDRMerge install issue, please ensure the binary path is proper'))
return
end
images = dt.gui.selection() --get selected images
if #images < 2 then --ensure enough images selected
dt.print(_('not enough images selected, select at least 2 images to merge'))
return
end
UpdateActivePreference() --save current gui elements to active preference so those values will be pre-loaded at next startup
--create image string and output path
HDRM.images_string = ''
local out_path = ''
local smallest_id = math.huge
local smallest_name = ''
local largest_id = 0
local source_raw = {}
for _,image in pairs(images) do --loop to concat the images string, also track the image indexes for use in creating the final image name (eg; IMG_1034-1037.dng)
local curr_image = image.path..os_path_seperator..image.filename
HDRM.images_string = HDRM.images_string..df.sanitize_filename(curr_image)..' '
out_path = image.path
_, source_name, source_id = GetFileName(image.filename)
source_id = tonumber(source_id)
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
-- GUI Elements --
local lbl_hdr = dt.new_widget('section_label'){
label = _('HDRMerge options')
}
temp = dt.preferences.read(mod, 'active_bps_ind', 'integer')
if not InRange(temp, 1, 3) then temp = 3 end
GUI.HDR.bps = dt.new_widget('combobox'){
label = _('bits per sample'),
tooltip =_('number of bits per sample in the output image'),
selected = temp,
'16','24','32',
changed_callback = function(self)
dt.preferences.write(mod, 'active_bps', 'integer', self.value)
dt.preferences.write(mod, 'active_bps_ind', 'integer', self.selected)
end,
reset_callback = function(self)
self.selected = 3
dt.preferences.write(mod, 'active_bps', 'integer', self.value)
dt.preferences.write(mod, 'active_bps_ind', 'integer', self.selected)
end
}
temp = dt.preferences.read(mod, 'active_size_ind', 'integer')
if not InRange(temp, 1, 3) then temp = 2 end
GUI.HDR.size = dt.new_widget('combobox'){
label = _('embedded preview size'),
tooltip =_('size of the embedded preview in output image'),
selected = temp,
_('none'),_('half'),_('full'),
changed_callback = function(self)
dt.preferences.write(mod, 'active_size', 'string', self.value)
dt.preferences.write(mod, 'active_size_ind', 'integer', self.selected)
end,
reset_callback = function(self)
self.selected = 2
dt.preferences.write(mod, 'active_size', 'string', self.value)
dt.preferences.write(mod, 'active_size_ind', 'integer', self.selected)
end
}
GUI.HDR.batch = dt.new_widget('check_button'){
label = _('batch mode'),
value = dt.preferences.read(mod, 'active_batch', 'bool'),
tooltip = _('enable batch mode operation \nNOTE: resultant files will NOT be auto-imported'),
clicked_callback = function(self)
dt.preferences.write(mod, 'active_batch', 'bool', self.value)
GUI.HDR.gap.sensitive = self.value
end,
reset_callback = function(self) self.value = false end
}
temp = dt.preferences.read(mod, 'active_gap', 'integer')
if not InRange(temp, 1, 3600) then temp = 3 end
GUI.HDR.gap = dt.new_widget('slider'){
label = _('batch gap [sec.]'),
tooltip = _('gap, in seconds, between batch mode groups'),
soft_min = 1,
soft_max = 30,
hard_min = 1,
hard_max = 3600,
step = 1,
digits = 0,
value = temp,
sensitive = GUI.HDR.batch.value,
reset_callback = function(self)
self.value = 3
end
}
local lbl_import = dt.new_widget('section_label'){
label = _('import options')
}
GUI.Target.style = dt.new_widget('combobox'){
label = _('apply style on import'),
tooltip = _('Apply selected style on auto-import to newly created image'),
selected = 1,
_('none'),
changed_callback = function(self)
dt.preferences.write(mod, 'active_style', 'string', self.value)
dt.preferences.write(mod, 'active_style_ind', 'integer', self.selected)
end,
reset_callback = function(self)
self.selected = 1
dt.preferences.write(mod, 'active_style', 'string', self.value)
dt.preferences.write(mod, 'active_style_ind', 'integer', self.selected)
end
}
for k=1, (styles_count-1) do
GUI.Target.style[k+1] = styles[k].name
end
temp = dt.preferences.read(mod, 'active_style_ind', 'integer')
if not InRange(temp, 1, styles_count) then temp = 1 end
GUI.Target.style.selected = temp
GUI.Target.copy_tags = dt.new_widget('check_button'){
label = _('copy tags'),
value = dt.preferences.read(mod, 'active_copy_tags', 'bool'),
tooltip = _('copy tags from first source image'),
clicked_callback = function(self) dt.preferences.write(mod, 'active_copy_tags', 'bool', self.value) end,
reset_callback = function(self) self.value = true end
}
temp = dt.preferences.read(mod, 'active_add_tags', 'string')
if temp == '' then temp = nil end
GUI.Target.add_tags = dt.new_widget('entry'){
tooltip = _('Additional tags to be added on import. Seperate with commas, all spaces will be removed'),
text = temp,
placeholder = _('Enter tags, seperated by commas'),
editable = true
}
GUI.run = dt.new_widget('button'){
label = _('merge'),
tooltip =_('run HDRMerge with the above specified settings'),
clicked_callback = function() main() end
}
GUI.exes.HDRMerge = dt.new_widget('file_chooser_button'){
title = _('Select HDRmerge executable'),
value = df.get_executable_path_preference(HDRM.name),
is_directory = false
}
GUI.exes.update = dt.new_widget('button'){
label = _('update'),
tooltip =_('update the binary path with current value'),
clicked_callback = function() ExeUpdate({HDRM}) end
}
GUI.options = dt.new_widget('box'){
orientation = 'vertical',
lbl_hdr,
GUI.HDR.bps,
GUI.HDR.size,
GUI.HDR.batch,
GUI.HDR.gap,
lbl_import,
GUI.Target.style,
GUI.Target.copy_tags,
GUI.Target.add_tags,
GUI.run
}
local exes_box = dt.new_widget('box'){
orientation = 'vertical',
GUI.exes.HDRMerge,
GUI.exes.update
}
GUI.stack = dt.new_widget('stack'){
GUI.options,
exes_box
}
if dt.preferences.read(mod, 'bin_exists', 'bool') then
GUI.stack.active = 1
else
GUI.stack.active = 2
end
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
}
)

234
contrib/LabelsToTags.lua Normal file
View File

@@ -0,0 +1,234 @@
--[[
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("3.0.0", "LabelsToTags")
-- Lua 5.3 no longer has "unpack" but "table.unpack"
unpack = unpack or table.unpack
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" .. (#(darktable.gui.action_images) == 1 and "" or "s") .. ")",#(darktable.gui.action_images)),true)
job.percent = 0.0
local pctIncrement = 1.0 / #(darktable.gui.action_images)
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
local 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
--[[
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" } })
]]
darktable.register_lib(LIB_ID,"labels to tags",true,true,{
[darktable.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER",20},
},my_widget,nil,nil)

111
contrib/OpenInExplorer.lua Normal file
View File

@@ -0,0 +1,111 @@
--[[
OpenInExplorer plugin for darktable
copyright (c) 2018 Kevin Ertel
Update 2020 and macOS support by Volker Lenhardt
Linux support 2020 by Bill Ferguson
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[About this plugin
This plugin adds the module "OpenInExplorer" to darktable's lighttable view.
----REQUIRED SOFTWARE----
Apple macOS, Microsoft Windows or Linux
----USAGE----
Install: (see here for more detail: https://github.com/darktable-org/lua-scripts )
1) Copy this file into your "lua/contrib" folder where all other scripts reside.
2) Require this file in your luarc file, as with any other dt plug-in
Select the photo(s) you wish to find in your operating system's file manager and press "show in file explorer" in the "selected images" section.
- Nautilus (Linux), Explorer (Windows), and Finder (macOS before Catalina) will open one window for each selected image at the file's location. The file name will be highlighted.
- On macOS 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.
----KNOWN ISSUES----
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
local dsys = require "lib/dtutils.system"
local gettext = dt.gettext
--Check API version
du.check_min_api_version("5.0.0", "OpenInExplorer")
gettext.bindtextdomain("OpenInExplorer",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("OpenInExplorer", msgid)
end
local act_os = dt.configuration.running_os
local PS = act_os == "windows" and "\\" or "/"
--Detect OS and quit if it is not supported--
local proper_install = true
if act_os ~= "macos" and act_os ~= "windows" and act_os ~= "linux" then
proper_install = false
dt.print_error(_('OpenInExplorer plug-in only supports Linux, macOS, and Windows at this time'))
return
end
--Format strings for the commands to open the corresponding OS' file manager
local fmng_cmd = {}
fmng_cmd.linux = [[busctl --user call org.freedesktop.FileManager1 /org/freedesktop/FileManager1 org.freedesktop.FileManager1 ShowItems ass %d %s""]]
fmng_cmd.macos = 'open -Rn %s'
fmng_cmd.windows = 'explorer.exe /select, %s'
--The working function that opens the file manager windows with the selected image file names highlighted.
local function open_in_fmanager(op_sys, fmcmd)
local images = dt.gui.selection()
local curr_image, run_cmd, file_uris = '', '', ''
if #images == 0 then
dt.print(_('Please select an image'))
elseif #images <= 15 then
for _,image in pairs(images) do
curr_image = image.path..PS..image.filename
if op_sys == 'linux' then
file_uris = file_uris .. df.sanitize_filename("file://" .. curr_image) .. " "
dt.print_log("file_uris is " .. file_uris)
else
run_cmd = string.format(fmcmd, df.sanitize_filename(curr_image))
dt.print_log("OpenInExplorer run_cmd = "..run_cmd)
dsys.external_command(run_cmd)
end
end
if op_sys == 'linux' then
run_cmd = string.format(fmcmd, #images, file_uris)
dt.print_log("OpenInExplorer run_cmd = "..run_cmd)
dsys.external_command(run_cmd)
end
else
dt.print(_('Please select fewer images (max 15)'))
end
end
-- GUI --
if proper_install then
dt.gui.libs.image.register_action(
_("show in file explorer"),
function() open_in_fmanager(act_os, fmng_cmd[act_os]) end,
_("Opens the file manager at the selected image's location")
)
end

212
contrib/RL_out_sharp.lua Normal file
View File

@@ -0,0 +1,212 @@
--[[
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)
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("5.0.0", MODULE_NAME)
-- OS compatibility
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
-- translation
local gettext = dt.gettext
gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS)
local function _(msgid)
return gettext.dgettext(MODULE_NAME, msgid)
end
-- initialize module preferences
if not dt.preferences.read(MODULE_NAME, "initialized", "bool") then
dt.preferences.write(MODULE_NAME, "sigma", "string", "0.7")
dt.preferences.write(MODULE_NAME, "iterations", "string", "10")
dt.preferences.write(MODULE_NAME, "jpg_quality", "string", "95")
dt.preferences.write(MODULE_NAME, "initialized", "bool", true)
end
-- 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(_("sharpening image ")..i.." ...")
-- create unique filename
new_name = output_folder..PS..df.get_basename(temp_name)..".jpg"
new_name = df.create_unique_filename(new_name)
-- 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
-- delete temp image
os.remove(temp_name)
end
dt.print(_("finished exporting"))
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.1,
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")
-- end of script --------------------------------------------------------------
-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua
-- kate: hl Lua;

View File

@@ -37,13 +37,14 @@ GPLv2
]]
local darktable = require "darktable"
local du = require "lib/dtutils"
local filelib = require "lib/dtutils.file"
-- Forward declare the functions
local autostyle_apply_one_image,autostyle_apply_one_image_event,autostyle_apply,exiftool_attribute,capture
-- 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})
du.check_min_api_version("2.0.2", "autostyle")
-- Receive the event triggered
function autostyle_apply_one_image_event(event,image)
@@ -58,18 +59,19 @@ function autostyle_apply_one_image (image)
-- 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
return 0
end
if (not value) then
darktable.print("value to match not found in " .. darktable.preferences.read("autostyle","exif_tag","string"))
return
return 0
end
if (not style_name) then
darktable.print("style name not found in " .. darktable.preferences.read("autostyle","exif_tag","string"))
return
return 0
end
if not checkIfBinExists("exiftool") then
return
if not filelib.check_if_bin_exists("exiftool") then
darktable.print("Can't find exiftool")
return 0
end
@@ -83,28 +85,37 @@ function autostyle_apply_one_image (image)
end
if (not style) then
darktable.print("style not found for autostyle: " .. style_name)
return 0
end
-- Apply the style to image, if it is tagged
local ok,auto_dr_attr= pcall(exiftool_attribute,image.path .. '/' .. image.filename,tag)
--darktable.print_error("dr_attr:" .. auto_dr_attr)
-- If the lookup fails, stop here
if (not ok) then
return
darktable.print("Couldn't get attribute " .. auto_dr_attr .. " from exiftool's output")
return 0
end
if auto_dr_attr==value then
-- darktable.print("Image " .. image.filename .. ": autostyle automatically applied " .. darktable.preferences.read("autostyle","exif_tag","string") )
darktable.print_log("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)
return 1
else
darktable.print_log("Image " .. image.filename .. ": autostyle not applied, exif tag " .. darktable.preferences.read("autostyle","exif_tag","string") .. " not matched: " .. auto_dr_attr)
return 0
end
end
function autostyle_apply( shortcut)
local images = darktable.gui.action_images
local images_processed =0
local images_submitted =0
for _,image in pairs(images) do
autostyle_apply_one_image(image)
images_submitted = images_submitted +1
images_processed = images_processed + autostyle_apply_one_image(image)
end
darktable.print("Applied auto style to " .. images_processed .. " out of " .. images_submitted .. " image(s)")
end
-- Retrieve the attribute through exiftool
@@ -113,10 +124,11 @@ function exiftool_attribute(path,attr)
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 .. ">")
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
@@ -124,7 +136,7 @@ end
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'))

View File

@@ -37,12 +37,13 @@
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local gettext = dt.gettext
-- not a number
local NaN = 0/0
dt.configuration.check_version(...,{3,0,0},{4,0,0})
du.check_min_api_version("3.0.0", "clear_GPS")
-- Tell gettext where to find the .mo file translating messages for a particular domain

View File

@@ -37,10 +37,12 @@ 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})
du.check_min_api_version("3.0.0", "copy_attach_detach_tags")
-- 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/")

View File

@@ -33,9 +33,10 @@ 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("2.0.0", "cr2hdr")
local queue = {}
local processed_files = {}
@@ -60,7 +61,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

1128
contrib/enfuseAdvanced.lua Normal file

File diff suppressed because it is too large Load Diff

438
contrib/ext_editor.lua Normal file
View File

@@ -0,0 +1,438 @@
--[[
DESCRIPTION
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 lighttable module "external editors", 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 lighttable/external editors, 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
* select an image for editing with en external program, and:
* in lighttable/external editors, 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 lighttable/external editors, 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
* in lighttable select target storage "collection"
* enter in darkroom
* to create an export or a copy press CRTL+E
* use the shortcut to edit the current image with the corresponding external editor
* overwite the file, quit the external program
* the darkroom view 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
TODO
* send multiple images to the same program, maybe
BUGS, COMMENTS, SUGGESTIONS
* send to Marco Carrarini, marco.carrarini@gmail.com
CHANGES
* 20191224 - initial version
* 20191227 - added button "update list", better error handling, fixed bug with groups/tags in "edit"
]]
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"
-- check API version
du.check_min_api_version("5.0.2", MODULE_NAME) -- darktable 3.0
-- OS compatibility
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
-- translation
local gettext = dt.gettext
gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS)
local function _(msgid)
return gettext.dgettext(MODULE_NAME, msgid)
end
-- 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"}
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(n_entries.._(" editors configured")) 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]
-- check if external program executable exists, return if not
if not df.check_if_bin_exists(bin) then
dt.print(friendly_name.._(" not found"))
return
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
while df.check_if_file_exists(df.sanitize_filename(new_name)) do
new_name = df.filename_increment(new_name)
-- limit to 50 more exports of the original export
if string.match(df.get_basename(new_name), "_%d%d$") == "_50" then break end
end
-- physical copy, check result, return if error
local copy_success = df.file_copy(name, new_name)
if not copy_success then
dt.print(_("error copying file ")..name)
return
end
end
-- launch the external editor, check result, return if error
local run_cmd = bin.." "..df.sanitize_filename(new_name)
dt.print(_("launching ")..friendly_name.."...")
local result = dtsys.external_command(run_cmd)
if result ~= 0 then
dt.print(_("error launching ")..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
-- refresh darkroom view
if dt.gui.current_view() == dt.gui.views.darkroom then
dt.gui.views.darkroom.display_image(new_image)
end
end
-- restore image tags, rating and color, must be put after refresh darkroom view
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)
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 new_name, new_image, result
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)
while df.check_if_file_exists(df.sanitize_filename(new_name)) do
new_name = df.filename_increment(new_name)
-- limit to 50 more exports of the original export
if string.match(df.get_basename(new_name), "_%d%d$") == "_50" then break end
end
-- move image to collection folder, check result, return if error
move_success = df.file_move(temp_name, new_name)
if not move_success then
dt.print(_("error moving file ")..temp_name)
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
-- register new storage -------------------------------------------------------
-- note that placing this declaration later makes the export selected module
-- not to remember the choice "collection" when restarting DT, don't know why
dt.register_storage("exp2coll", _("collection"), null, export2collection)
-- 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
}
-- register new module "external editors" in lighttable ------------------------
dt.register_lib(
MODULE_NAME,
_("external editors"),
true, -- expandable
false, -- resetable
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}},
dt.new_widget("box") {
orientation = "vertical",
combobox,
box1
},
nil, -- view_enter
nil -- view_leave
)
-- initialize list of programs and widgets ------------------------------------
UpdateProgramList(combobox, button_edit, button_edit_copy, false)
-- register the new preferences -----------------------------------------------
for i = MAX_EDITORS, 1, -1 do
dt.preferences.register(MODULE_NAME, "program_path_"..i, "file",
_("executable for external editor ")..i,
_("select executable for external editor") , _("(None)"))
dt.preferences.register(MODULE_NAME, "program_name_"..i, "string",
_("name of external editor ")..i,
_("friendly name of external editor"), "")
end
-- register the new shortcuts -------------------------------------------------
for i = 1, MAX_EDITORS do
dt.register_event("shortcut", program_shortcut, _("edit with program ")..string.format("%02d", i))
end
-- end of script --------------------------------------------------------------
-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua
-- kate: hl Lua;

View File

@@ -0,0 +1,491 @@
--[[
Face recognition for darktable
Copyright (c) 2017 Sebastian Witt
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[
face_recognition
Add a new storage option to send images to face_recognition.
Images are exported to darktable tmp dir first.
A directory with known faces must exist, the image name are the
tag names which will be used.
Multiple images for one face can exist, add a number to it, the
number will be removed from the tag, for example:
People|IknowYou1.jpg
People|IknowYou2.jpg
People|Another.jpg
People|Youtoo.jpg
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
* https://github.com/ageitgey/face_recognition
* https://github.com/darktable-org/lua-scripts/tree/master/lib
USAGE
* require this file from your main luarc config file.
This plugin will add a new storage option and calls face_recognition after export.
]]
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
-- constants
local MODULE = "face_recognition"
local PS = dt.configuration.running_os == "windows" and '\\' or '/'
local OUTPUT = dt.configuration.tmp_dir .. PS .. "facerecognition.txt"
-- namespace
local fc = {}
-- ensure we meet the minimum api
du.check_min_api_version("5.0.0", "face_recognition")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("face_recognition", dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("face_recognition", msgid)
end
local function build_image_table(images)
local image_table = {}
local file_extension = ""
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
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
local function ignoreByTag (image, ignoreTags)
local tags = image:get_tags ()
local ignoreImage = false
-- For each image tag
for _,t in ipairs (tags) do
-- Check if it contains a ignore tag
for _,it in ipairs (ignoreTags) do
if string.find (t.name, it, 1, true) then
-- The image has ignored tag attached
ignoreImage = true
dt.print_log ("Face recognition: Ignored tag: " .. it .. " found in " .. image.id .. ":" .. t.name)
end
end
end
return ignoreImage
end
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(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_log ("Face recognition: Ignore tag: " .. tag)
end
-- list of exported images
local image_table, cnt = build_image_table(dt.gui.action_images)
if cnt > 0 then
local success = do_export(image_table, cnt)
if success then
-- do the face recognition
local img_list = {}
for v,_ in pairs(image_table) do
table.insert (img_list, v)
end
-- 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")
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
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
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
-- 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
}
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),
}
dt.register_lib(
"face_recognition", -- 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.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
--
-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua

View File

@@ -24,10 +24,11 @@ Dependencies:
--]]
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
local gettext = dt.gettext
dt.configuration.check_version(..., {4,0,0})
du.check_min_api_version("4.0.0", "fujifilm_ratings")
gettext.bindtextdomain("fujifilm_ratings", dt.configuration.config_dir.."/lua/locale/")

View File

@@ -33,11 +33,12 @@ USAGE
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
require "official/yield"
local gettext = dt.gettext
dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0})
du.check_min_api_version("3.0.0", "geoJSON_export")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("geoJSON_export",dt.configuration.config_dir.."/lua/locale/")

View File

@@ -26,11 +26,12 @@ require "geoToolbox"
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
require "official/yield"
local gettext = dt.gettext
dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0})
du.check_min_api_version("3.0.0", "geoToolbox")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("geoToolbox",dt.configuration.config_dir.."/lua/locale/")

View File

@@ -64,181 +64,56 @@
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
require "official/yield"
local dtsys = require "lib/dtutils.system"
local gettext = dt.gettext
local gimp_widget = nil
dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0})
du.check_min_api_version("5.0.0", "gimp")
-- 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
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
end
local function filename_increment(filepath)
-- break up the filepath into parts
local path = get_path(filepath)
local basename = get_basename(filepath)
local filetype = get_filetype(filepath)
-- check to see if we've incremented before
local increment = string.match(basename, "_(%d-)$")
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)
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))
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 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 function gimp_edit(storage, image_table, extra_data) --finalize
if not df.check_if_bin_exists("gimp") then
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
gimp_executable = "open -W -a " .. gimp_executable
end
-- list of exported images
local img_list
@@ -246,18 +121,18 @@ 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..."))
local gimpStartCommand
gimpStartCommand = "gimp "..img_list
gimpStartCommand = gimp_executable .. " " .. img_list
dt.print_error(gimpStartCommand)
dt.print_log(gimpStartCommand)
dt.control.execute( gimpStartCommand)
dtsys.external_command(gimpStartCommand)
-- for each of the image, exported image pairs
-- move the exported image into the directory with the original
@@ -266,28 +141,28 @@ local function gimp_edit(storage, image_table, extra_data) --finalize
for image,exported_image in pairs(image_table) do
local myimage_name = image.path .. "/" .. get_filename(exported_image)
local myimage_name = image.path .. "/" .. df.get_filename(exported_image)
while checkIfFileExists(myimage_name) do
myimage_name = filename_increment(myimage_name)
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(get_basename(myimage_name), "_(d-)$") == "99" then
if string.match(df.get_basename(myimage_name), "_(d-)$") == "99" then
break
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")
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.print_log("attaching tag")
dt.tags.attach(tag,myimage)
end
end
@@ -297,6 +172,13 @@ local function gimp_edit(storage, image_table, extra_data) --finalize
end
-- Register
dt.register_storage("module_gimp", _("Edit with GIMP"), show_status, gimp_edit)
local executables = {"gimp"}
if dt.configuration.running_os ~= "linux" then
gimp_widget = df.executable_path_widget(executables)
end
dt.register_storage("module_gimp", _("Edit with GIMP"), show_status, gimp_edit, nil, nil, gimp_widget)
--

157
contrib/gpx_export.lua Normal file
View File

@@ -0,0 +1,157 @@
--[[
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 <http://www.gnu.org/licenses/>.
]]
--[[
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 <trk> is generated in the gpx file.
]]
local dt = require "darktable"
local df = require "lib/dtutils.file"
local dl = require "lib/dtutils"
local gettext = dt.gettext
dl.check_min_api_version("3.0.0", "gpx-export")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("gpx_export",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("gpx_export", msgid)
end
local 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 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n"
gpx_file = gpx_file.."<gpx xmlns=\"http://www.topografix.com/GPX/1/1\" creator=\"Darktable GPX Exporter\"\n"
gpx_file = gpx_file.."version=\"1.1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
gpx_file = gpx_file.."xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">\n"
for key, folder in dl.spairs(segments) do
gpx_file = gpx_file.."\t<trk>\n"
gpx_file = gpx_file.."\t\t<name>"..key.."</name>\n";
gpx_file = gpx_file.."\t\t<trkseg>\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<trkpt lat=\""..string.gsub(tostring(image.latitude), ",", ".").."\" lon=\""..string.gsub(tostring(image.longitude), ",", ".").."\">\n"
gpx_file = gpx_file.."\t\t\t\t<time>"..os.date("!%Y-%m-%dT%H:%M:%SZ", my_timestamp).."</time>\n"
gpx_file = gpx_file.."\t\t\t\t<name>"..image.path.."/"..image.filename.."</name>\n"
gpx_file = gpx_file.."\t\t\t</trkpt>\n"
end
gpx_file = gpx_file.."\t\t</trkseg>\n";
gpx_file = gpx_file.."\t</trk>\n";
end
job.valid = false
gpx_file = gpx_file.."</gpx>\n";
local file = io.open(path, "w")
if (file == nil) then
dt.print(_("invalid path: ")..path)
else
file:write(gpx_file)
file:close()
dt.print(_("gpx file created: ")..path)
end
end
dt.register_lib(
"gpx_exporter",
"gpx export",
true, -- expandable
true, -- resetable
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers
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
},
},
nil,-- view_enter
nil -- view_leave
)

View File

@@ -28,18 +28,34 @@ 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"
local log = require "lib/dtutils.log"
local dtsys = require "lib/dtutils.system"
require "official/yield"
local gettext = dt.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"}
-- 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("5.0.0", "hugin")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("hugin",dt.configuration.config_dir.."/lua/locale/")
@@ -48,65 +64,167 @@ local function _(msgid)
return gettext.dgettext("hugin", msgid)
end
local function user_preference_changed(widget)
user_prefer_gui = widget.value
dt.preferences.write(namespace, user_pref_str, "bool", user_prefer_gui)
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("exporting to hugin: "..tostring(number).."/"..tostring(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<b.filename end);
local first_file
for _, k in ipairs(img_set) do
first_file = k
break
end
dt.print(_("Running Assistent"))
huginStartCommand = "hugin_executor --assistant "..dt.configuration.tmp_dir.."/project.pto"
dt.control.execute( huginStartCommand)
if first_file == nil then
dt.print("no file selected")
return
end
huginStartCommand = "hugin "..dt.configuration.tmp_dir.."/project.pto"
local pto_path = dt.configuration.tmp_dir..'/project.pto'
log.msg(log.info, "pto_path is ", pto_path)
local filepath = df.split_filepath(first_file.filename)
log.msg(log.info, "filepath is ", filepath)
local tmp_filename = filepath['basename'].."_pano.tif"
log.msg(log.info, "tmp_filename is ", tmp_filename)
-- some versions of hugin_executor don't seem to output the result
-- to the --prefix location, so assume we are working in the tmp
-- directory and then move the file to the original file location afterwards
local src_path = dt.configuration.tmp_dir.."/"..tmp_filename
log.msg(log.info, "src_path is ", src_path)
local dst_path = first_file.path..'/'..tmp_filename
log.msg(log.info, "dst_path is ", dst_path)
if df.check_if_file_exists(dst_path) then
dst_path = df.create_unique_filename(dst_path)
log.msg(log.info, "dst_path updated to ", dts_path)
end
dt.print(_("will try to stitch now"))
local huginStartCommand = nil
if (cmd_line_available and not user_prefer_gui) then
log.msg(log.info, "using the command line tools to create panorama")
huginStartCommand = pto_gen.." "..img_list.." -o "..pto_path
dt.print(_("creating pto file"))
log.msg(log.info, "pto creation command is ", huginStartCommand)
dtsys.external_command(huginStartCommand)
dt.print(_("running assistant"))
huginStartCommand = hugin_executor.." --assistant "..pto_path
dtsys.external_command(huginStartCommand)
huginStartCommand = hugin_executor..' --stitching --prefix=' .. "'" .. src_path .. "'" .. ' ' .. pto_path
log.msg(log.info, "command line huginStartCommand is ", huginStartCommand)
elseif gui_available then
if (user_prefer_gui) then
dt.print(_("launching hugin"))
else
huginStartCommand = "hugin "..img_list
dt.print(_("unable to find command line tools, launching hugin"))
end
-- the gui produces a differnt output filename than the command line
src_path = dt.configuration.tmp_dir .. "/" .. df.get_basename(img_set[1].filename) .. " - " .. df.get_basename(img_set[#img_set].filename) .. ".tif"
log.msg(log.info, "set src_path to ", src_path)
huginStartCommand = hugin..' '..img_list
log.msg(log.info, "gui huginStartCommand is ", huginStartCommand)
else
dt.print(_("hugin isn't available."))
end
dt.print_error(huginStartCommand)
if not dt.control.execute(huginStartCommand)
then
dt.print(_("Command hugin failed ..."))
if not (huginStartCommand==nil) then
if not dtsys.external_command(huginStartCommand) then
dt.print(_("hugin failed ..."))
log.msg(log.error, huginStartCommand, " failed")
else
if df.check_if_file_exists(src_path) then
log.msg(log.debug, "found ", src_path, " importing to ", dst_path)
df.file_move(src_path, dst_path)
dt.print(_("importing file "..dst_path))
dt.database.import(dst_path)
end
end
log.log_level(saved_log_level)
end
-- cleanup the temp files
for k,v in pairs(image_table) do
os.remove(v)
end
end
-- Register
dt.register_storage("module_hugin", _("Hugin Panorama"), show_status, create_panorama)
if dt.configuration.running_os ~= "linux" then
exec_widget = df.executable_path_widget(executable_table)
end
hugin_widget = dt.new_widget("box") {
orientation = "vertical",
dt.new_widget("check_button")
{
label = _(" launch hugin gui"),
value = user_prefer_gui,
tooltip = _('launch hugin in gui mode'),
clicked_callback = user_preference_changed
},
exec_widget
}
dt.register_storage(namespace, _("hugin panorama"), show_status, create_panorama, nil, nil, hugin_widget)
--
-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua

587
contrib/image_stack.lua Normal file
View File

@@ -0,0 +1,587 @@
--[[
image_stack.lua - process a stack of images
Copyright (C) 2018 Bill Ferguson <wpferguson@gmail.com>.
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 <http://www.gnu.org/licenses/>.
]]
--[[
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
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("5.0.0", "image_stack")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("image_stack",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("image_stack", msgid)
end
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-- GUI definitions
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 for first'),
}
local chkbtn_optimize_field = dt.new_widget("check_button"){
label = _('optimize field of view for all images'),
value = dt.preferences.read("align_image_stack", "def_optimize_field", "bool"),
tooltip =_('optimize field of view for all images, except for first. \nUseful for aligning focus stacks (DFF) with slightly \ndifferent magnification.'),
}
local chkbtn_optimize_image_center = dt.new_widget("check_button"){
label = _('optimize image center shift for all images'),
value = dt.preferences.read("align_image_stack", "def_optimize_image_center", "bool"),
tooltip =_('optimize image center shift for all images, \nexcept for 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
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-- 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 remaping'), -- 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)

View File

@@ -1,7 +1,7 @@
--[[
This file is part of darktable,
Copyright 2016 by Tobias Jakobs.
Copyright 2016 by Erik Augustin.
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
@@ -20,28 +20,30 @@
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 ds = require "lib/dtutils.string"
local dsys = require "lib/dtutils.system"
local gettext = dt.gettext
dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0})
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
du.check_min_api_version("5.0.0", kml_export)
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("kml_export",dt.configuration.config_dir.."/lua/locale/")
@@ -50,157 +52,61 @@ local function _(msgid)
return gettext.dgettext("kml_export", msgid)
end
-- Sort a table
local function spairs(_table, order) -- Code copied from http://stackoverflow.com/questions/15706270/sort-a-table-in-lua
-- collect the keys
local keys = {}
for _key in pairs(_table) do keys[#keys + 1] = _key end
-- 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
-- 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
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
-- 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
end
-- Escape XML characters
-- Keep &amp; 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,"&", "&amp;")
str = string.gsub(str,"\"", "&quot;")
str = string.gsub(str,"'", "&apos;")
str = string.gsub(str,"<", "&lt;")
str = string.gsub(str,">", "&gt;")
return str
return filename
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"))
local magickPath
if dt.configuration.running_os == "linux" then
magickPath = 'convert'
else
magickPath = dt.preferences.read("kml_export","magickPath","string")
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
end
dt.print_error("Will try to export KML file now")
end
dt.print_log("Will try to export KML file now")
local imageFoldername
if ( dt.preferences.read("kml_export","CreateKMZ","bool") == true ) then
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/"
local mkdirCommand = "mkdir -p "..exportDirectory.."/"..imageFoldername
dt.control.execute(mkdirCommand)
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
@@ -209,15 +115,20 @@ local function create_kml_file(storage, image_table, extra_data)
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
-- 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 convertToThumbCommand = "convert -size 96x96 "..exported_image.." -resize 92x92 -mattecolor \"#FFFFFF\" -frame 2x2 +profile \"*\" "..exportDirectory.."/"..imageFoldername.."thumb_"..filename..".jpg"
dt.control.execute(convertToThumbCommand)
local convertToThumbCommand = ds.sanitize(magickPath) .. " -size 96x96 " .. exported_image .. " -resize 92x92 -mattecolor \"#FFFFFF\" -frame 2x2 +profile \"*\" " .. exportDirectory .. PS .. imageFoldername .. "thumb_" .. filename .. ".jpg"
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)
@@ -228,7 +139,19 @@ local function create_kml_file(storage, image_table, extra_data)
-- Strip accents from the filename, because GoogleEarth can't open them
-- https://github.com/darktable-org/lua-scripts/issues/54
filmName = string.stripAccents(filmName)
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
exportKMLFilename = filmName..".kml"
@@ -246,6 +169,8 @@ local function create_kml_file(storage, image_table, extra_data)
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*$")
@@ -256,12 +181,12 @@ local function create_kml_file(storage, image_table, extra_data)
local image_title, image_description
if (image.title and image.title ~= "") then
image_title = string.escapeXmlCharacters(image.title)
image_title = ds.escape_xml_characters(image.title)
else
image_title = image.filename
image_title = filename..extension
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)
image_description = ds.escape_xml_characters(image.description)
kml_file = kml_file.." <name>"..image_title.."</name>\n"
kml_file = kml_file.." <description>"..image_description.."</description>\n"
@@ -306,7 +231,7 @@ local function create_kml_file(storage, image_table, extra_data)
kml_file = kml_file.." <LineString>\n"
kml_file = kml_file.." <coordinates>\n"
for image,exported_image in spairs(image_table, function(t,a,b) return t[b] < t[a] end) do
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
@@ -314,9 +239,11 @@ local function create_kml_file(storage, image_table, extra_data)
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.." </coordinates>\n"
kml_file = kml_file.." </LineString>\n"
@@ -326,27 +253,31 @@ local function create_kml_file(storage, image_table, extra_data)
kml_file = kml_file.."</Document>\n"
kml_file = kml_file.."</kml>"
local file = io.open(exportDirectory.."/"..exportKMLFilename, "w")
local file = io.open(exportDirectory..PS..exportKMLFilename, "w")
file:write(kml_file)
file:close()
dt.print("KML file created in "..exportDirectory)
-- Compress the files to create a KMZ file
if ( dt.preferences.read("kml_export","CreateKMZ","bool") == true ) then
if ( dt.preferences.read("kml_export","CreateKMZ","bool") == true
and dt.configuration.running_os == "linux") then
exportDirectory = dt.preferences.read("kml_export","ExportDirectory","string")
local createKMZCommand = "zip --test --move --junk-paths "
createKMZCommand = createKMZCommand .."\""..exportDirectory.."/"..exportKMZFilename.."\" " -- KMZ filename
createKMZCommand = createKMZCommand .."\""..dt.configuration.tmp_dir.."/"..exportKMLFilename.."\" " -- KML file
createKMZCommand = createKMZCommand .."\""..exportDirectory..PS..exportKMZFilename.."\" " -- KMZ filename
createKMZCommand = createKMZCommand .."\""..dt.configuration.tmp_dir..PS..exportKMLFilename.."\" " -- KML file
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.."/"..imageFoldername.."thumb_"..filename..".jpg\" " -- thumbnails
createKMZCommand = createKMZCommand .."\""..dt.configuration.tmp_dir..PS..imageFoldername.."thumb_"..filename..".jpg\" " -- thumbnails
createKMZCommand = createKMZCommand .."\""..exported_image.."\" " -- images
end
end
@@ -356,39 +287,64 @@ local function create_kml_file(storage, image_table, extra_data)
-- Open the file with the standard programm
if ( dt.preferences.read("kml_export","OpenKmlFile","bool") == true ) then
local kmlFileOpenCommand
local path
if ( dt.preferences.read("kml_export","CreateKMZ","bool") == true ) then
kmlFileOpenCommand = "xdg-open "..exportDirectory.."/\""..exportKMZFilename.."\""
if ( dt.preferences.read("kml_export","CreateKMZ","bool") == true
and dt.configuration.running_os == "linux") then
path = exportDirectory..PS..exportKMZFilename
else
kmlFileOpenCommand = "xdg-open "..exportDirectory.."/\""..exportKMLFilename.."\""
end
dt.control.execute(kmlFileOpenCommand)
path = exportDirectory..PS..exportKMLFilename
end
dsys.launch_default_app(df.sanitize_filename(path))
end
end
-- Preferences
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 )
local handle = io.popen("xdg-user-dir DESKTOP")
local result = handle:read()
if (result == nil) then
result = ""
end
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",
"ExportDirectory",
"directory",
_("KML export: Export directory"),
_("A directory that will be used to export the KML/KMZ files"),
result )
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",
"CreatePath",
@@ -396,12 +352,22 @@ dt.preferences.register("kml_export",
_("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
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
-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua
-- kate: hl Lua;

View File

@@ -36,9 +36,10 @@ USAGE
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local gettext = dt.gettext
dt.configuration.check_version(...,{2,0,0},{3,0,0},{4,0,0},{5,0,0})
du.check_min_api_version("2.0.0", "passport_guide")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("passport_guide",dt.configuration.config_dir.."/lua/locale/")

View File

@@ -39,6 +39,7 @@ 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"
@@ -56,7 +57,7 @@ if not df.check_if_bin_exists("pdflatex") then
return
end
dt.configuration.check_version(...,{4,0,0},{5,0,0})
du.check_min_api_version("4.0.0", "pdf_slideshow")
dt.preferences.register
("pdf_slideshow","open with","string",

View File

@@ -44,12 +44,13 @@ 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})
du.check_min_api_version("3.0.0", "quicktag")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("quicktag",dt.configuration.config_dir.."/lua/locale/")

View File

@@ -39,9 +39,10 @@
]]
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("3.0.0", "rate_group")
local function apply_rating(rating)
local images = dt.gui.action_images

View File

@@ -1,3 +1,7 @@
--[[
Copyright (c) 2016 Sebastian Witt <se.witt@gmx.net>
Copyright (c) 2018 Bill Ferguson <wpferguson@gmail.com>
]]
--[[
Rename tags
@@ -18,17 +22,30 @@ 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("3.0.0", "rename-tags")
-- 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 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")
@@ -39,7 +56,7 @@ local function rename_tags(widget)
return
end
local Count = 0
local count = 0
-- Check if old tag exists
local ot = darktable.tags.find (old_tag.text)
@@ -58,29 +75,28 @@ 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 ("Renamed tags for " .. count .. " images")
old_tag.editable = true
new_tag.editable = true
-- reset the gui fields
rename_reset()
end
-- GUI
@@ -96,11 +112,6 @@ local new_widget = darktable.new_widget ("box") {
new_tag
}
local function rename_reset(widget)
old_tag.text = ''
new_tag.text = ''
end
local rename_widget = darktable.new_widget ("box") {
orientation = "vertical",
reset_callback = rename_reset,

View File

@@ -19,9 +19,10 @@ Enable selection of untagged images (darktable|* tags are ignored)
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local gettext = dt.gettext
dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0})
du.check_min_api_version("3.0.0", "select_untagged")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("select_untagged",dt.configuration.config_dir.."/lua/locale/")

View File

@@ -25,11 +25,12 @@ USAGE
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
require "official/yield"
local gettext = dt.gettext
dt.configuration.check_version(...,{2,0,2},{3,0,0},{4,0,0})
du.check_min_api_version("2.0.2", "slideshowMusic")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("slideshowMusic",dt.configuration.config_dir.."/lua/locale/")

358
contrib/transfer_hierarchy.lua Executable file
View File

@@ -0,0 +1,358 @@
--[[
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 LIB_ID = "transfer_hierarchy"
dtutils.check_min_api_version("5.0.0", LIB_ID)
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
darktable.gettext.bindtextdomain(LIB_ID, darktable.configuration.config_dir .. PATH_SEPARATOR .. "lua" .. PATH_SEPARATOR .. "locale" .. PATH_SEPARATOR)
local function _(msgid)
return darktable.gettext.dgettext(LIB_ID, msgid)
end
-- Header material: END
-- Helper functions: BEGIN
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
-- 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(_("transfer hierarchy: ERROR: could not create directory: " .. films[film]))
return
end
end
if not pathIsDirectory(films[film]) then
darktable.print(_("transfer hierarchy: ERROR: not a directory: " .. films[film]))
return
end
destFilms[film] = darktable.films.new(films[film])
if destFilms[film] == nil then
darktable.print(_("transfer hierarchy: ERROR: could not create film: " .. 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" .. (#(darktable.gui.action_images) == 1 and "" or "s") .. ")", #(darktable.gui.action_images)), true, stopTransfer)
job.percent = 0.0
local pctIncrement = 1.0 / #(darktable.gui.action_images)
for _,img in ipairs(darktable.gui.action_images) do
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 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
darktable.register_lib(LIB_ID,
"transfer hierarchy", true, true, {
[darktable.gui.views.lighttable] = { "DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 700 }
}, transfer_widget, nil, nil)

460
contrib/video_ffmpeg.lua Normal file
View File

@@ -0,0 +1,460 @@
--[[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 <http://www.gnu.org/licenses/>.
]]
--[[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
du.check_min_api_version("5.0.0")
local MODULE_NAME = "video_ffmpeg"
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS)
local function _(msgid)
return gettext.dgettext(MODULE_NAME, msgid)
end
---- DECLARATIONS
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 = {"15", "16", "23.98", "24", "25", "29,97", "30", "48", "50", "59.94", "60"}
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 = _("framerate"),
tooltip = _("select framerate 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(_("export ")..tostring(number).." / "..tostring(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
dt.register_storage(
"module_video_ffmpeg",
_(MODULE_NAME),
show_status,
finalize_export,
nil,
init_export,
module_widget
)

View File

@@ -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 <http://www.gnu.org/licenses/>.
]]
--[[
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)

View File

@@ -0,0 +1,98 @@
--[[
This file is part of darktable,
copyright (c) 2019 Bill Ferguson <wpferguson@gmail.com>
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[
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_mode") -- darktable 3.0
-- - - - - - - - - - - - - - - - - - - - - - - -
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
local MODULE_NAME = "darkroom"
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- T R A N S L A T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
local gettext = dt.gettext
gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS)
local function _(msgid)
return gettext.dgettext(MODULE_NAME, 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()
-- enter darkroom view
dt.gui.current_view(dt.gui.views.darkroom)
local max_images = 10
dt.print(_("Showing images, with a pause in between each"))
sleep(1500)
-- display first 10 images of collection pausing for a second between each
for i, img in ipairs(dt.collection) do
dt.print(_("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)

View File

@@ -53,7 +53,10 @@ 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")
-- Not translated Text
dt.print_error("Hello World!")

View File

@@ -27,7 +27,9 @@ 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"
du.check_min_api_version("2.0.0", "hello_world")
dt.print("hello, world")

View File

@@ -0,0 +1,198 @@
--[[
This file is part of darktable,
copyright (c) 2019 Bill Ferguson <wpferguson@gmail.com>
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[
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 "/"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- T R A N S L A T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
local gettext = dt.gettext
gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS)
local function _(msgid)
return gettext.dgettext(MODULE_NAME, 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",
}
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(_("set lighttable layout to " .. layout))
sleep(1500)
for i = 1, 10 do
dt.gui.libs.lighttable_mode.zoom_level(i)
dt.print(_("Set zoom level to " .. i))
sleep(1500)
end
for i = 9, 1, -1 do
dt.gui.libs.lighttable_mode.zoom_level(i)
dt.print(_("Set zoom level to " .. i))
sleep(1500)
end
end
-- cycle through sorts
dt.print(_("lighttable sorting demonstration"))
dt.gui.libs.lighttable_mode.layout("DT_LIGHTTABLE_LAYOUT_FILEMANAGER")
dt.gui.libs.lighttable_mode.zoom_level(5)
for n, sort in ipairs(sorts) do
dt.gui.libs.filter.sort(sort)
dt.print(_("set lighttable sort to " .. sort))
sleep(1500)
for m, sort_order in ipairs(sort_orders) do
dt.gui.libs.filter.sort_order(sort_order)
dt.print(_("sort order set to " .. 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(_("set filter to " .. rating))
sleep(1500)
for m, rating_comparator in ipairs(rating_comparators) do
dt.gui.libs.filter.rating_comparator(rating_comparator)
dt.print(_("set rating comparator to " .. 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)

View File

@@ -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,58 @@ 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("3.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"}
-- translation
--https://www.darktable.org/lua-api/ar01s02s54.html.php
-- https://www.darktable.org/lua-api/index.html#darktable_gettext
local gettext = dt.gettext
gettext.bindtextdomain("moduleExample", dt.configuration.config_dir .. "/lua/locale/")
local function _(msgid)
return gettext.dgettext("moduleExample", msgid)
end
-- https://www.darktable.org/lua-api/types_lua_check_button.html
local check_button = dt.new_widget("check_button"){label = _("MyCheck_button"), value = true}
-- https://www.darktable.org/lua-api/types_lua_combobox.html
local combobox = dt.new_widget("combobox"){label = _("MyCombobox"), 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 = _("MyFile_chooser_button"), -- The title of the window when choosing a file
value = "", -- The currently selected file
is_directory = false -- True if the file chooser button only allows directories to be selecte
}
-- https://www.darktable.org/lua-api/types_lua_label.html
local label = dt.new_widget("label")
label.label = "MyLabel" -- This is an alternative way to the "{}" syntax to set a property
label.label = _("MyLabel") -- This is an alternative way to the "{}" syntax to set a property
-- https://www.darktable.org/lua-api/types_lua_separator.html
local separator = dt.new_widget("separator"){}
-- https://www.darktable.org/lua-api/types_lua_slider.html
local slider = dt.new_widget("slider")
{
label = "MySlider",
label = _("MySlider"),
soft_min = 10, -- The soft minimum value for the slider, the slider can't go beyond this point
soft_max = 100, -- The soft maximum value for the slider, the slider can't go beyond this point
hard_min = 0, -- The hard minimum value for the slider, the user can't manually enter a value beyond this point
@@ -70,55 +90,22 @@ 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"
}
-- https://www.darktable.org/lua-api/index.html#darktable_register_lib
dt.register_lib(
"exampleModule", -- Module name
"exampleModule", -- 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 = "MyButton",
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
)
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")
dt.print(_("Button clicked"))
end
},
check_button,
@@ -132,7 +119,6 @@ else
nil,-- view_enter
nil -- view_leave
)
end
-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua
-- kate: hl Lua;

244
examples/multi_os.lua Normal file
View File

@@ -0,0 +1,244 @@
--[[
This file is part of darktable,
copyright (c) 2017,2018 Bill Ferguson <wpferguson@gmail.com>
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[
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
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("multi_os",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("multi_os", 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("5.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(image.filename .. " is not a raw file. No image can be extracted") -- 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
--[[
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.
]]
local executable = "ufraw-batch"
local ufraw_batch_path_widget = dt.new_widget("file_chooser_button"){
title = _("Select ufraw-batch[.exe] executable"),
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
)
--[[
Add a button to the selected images module in lighttable
]]
dt.gui.libs.image.register_action(
_("extract embedded jpeg"),
function(event, images) extract_embedded_jpeg(images) end,
"extract embedded jpeg"
)
--[[
Add a shortcut
]]
dt.register_event(
"shortcut",
function(event, shortcut) extract_embedded_jpeg(dt.gui.action_images) end,
"extract embedded jpeg"
)

136
examples/panels_demo.lua Normal file
View File

@@ -0,0 +1,136 @@
--[[
This file is part of darktable,
copyright (c) 2019 Bill Ferguson <wpferguson@gmail.com>
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[
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("5.0.2", "panels_demo") -- darktable 3.0
-- - - - - - - - - - - - - - - - - - - - - - - -
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
local MODULE_NAME = "panels"
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
-- - - - - - - - - - - - - - - - - - - - - - - -
-- T R A N S L A T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
local gettext = dt.gettext
gettext.bindtextdomain(MODULE_NAME, dt.configuration.config_dir..PS.."lua"..PS.."locale"..PS)
local function _(msgid)
return gettext.dgettext(MODULE_NAME, 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 tme"))
sleep(1500)
for i = 1, #panels do
dt.print(_("Hiding " .. panels[i]))
dt.gui.panel_hide(panels[i])
sleep(1500)
end
-- display left, then top, then right, then bottom
dt.print(_("Make panels visible, one at a time"))
sleep(1500)
for i = #panels, 1, -1 do
dt.print(_("Showing " .. 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

View File

@@ -22,7 +22,9 @@ 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"
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

View File

@@ -20,7 +20,9 @@ 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")
-- Will print a string to the darktable control log (the long
-- overlayed window that appears over the main panel).

View File

@@ -26,7 +26,9 @@ prints the operating system
]]
local dt = require "darktable"
dt.configuration.check_version(...,{5,0,0})
local du = require "lib/dtutils"
du.check_min_api_version("5.0.0", "running_os")
dt.print("You are running: "..dt.configuration.running_os)

View File

@@ -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 <http://www.gnu.org/licenses/>.
]]
--[[
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

View File

@@ -32,7 +32,42 @@ 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 min_api > current_api then
dt.print_error("This application is written for lua api version " .. min_api .. " or later.")
dt.print_error("The current lua api version is " .. current_api)
dt.print("ERROR: " .. script_name .. " failed to load. Lua API version " .. min_api .. " or later required.")
error("Minimum API " .. min_api .. " not met for " .. script_name .. ".", 0)
end
end
dtutils.libdoc.functions["split"] = {
Name = [[split]],
@@ -215,4 +250,37 @@ 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 <script name> only runs on windows")
return
end]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils.check_os(operating_systems)
for _,os in pairs(operating_systems) do
if dt.configuration.running_os == os then
return true
end
end
return false
end
return dtutils

View File

@@ -1,5 +1,8 @@
local dtutils_file = {}
local dt = require "darktable"
local du = require "lib/dtutils"
local ds = require "lib/dtutils.string"
local dsys = require "lib/dtutils.system"
local log = require "lib/dtutils.log"
@@ -23,7 +26,7 @@ dtutils_file.libdoc = {
local gettext = dt.gettext
dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0})
du.check_min_api_version("5.0.0", "dtutils.file")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("dtutils.file",dt.configuration.config_dir.."/lua/locale/")
@@ -32,16 +35,55 @@ local function _(msgid)
return gettext.dgettext("dtutils.file", msgid)
end
dtutils_file.libdoc.functions["check_if_bin_exists"] = {
Name = [[check_if_bin_exists]],
Synopsis = [[check if an executable is in the path]],
--[[
local function to run a test command on windows and return a true if it succeeds
instead of returning true if it runs
]]
local function _win_os_execute(cmd)
local result = nil
local p = io.popen(cmd)
local output = p:read("*a")
p:close()
if string.match(output, "true") then
result = true
else
result = false
end
return result
end
--[[
local function to determine if a path name is a windows executable
]]
local function _is_windows_executable(path)
local result = false
if dtutils_file.test_file(path, "f") then
if string.match(path, ".exe$") or string.match(path, ".EXE$") or
string.match(path, ".com$") or string.match(path, ".COM$") or
string.match(path, ".bat$") or string.match(path, ".BAT$") or
string.match(path, ".cmd$") or string.match(path, ".CMD$") then
result = true
end
end
return result
end
dtutils_file.libdoc.functions["test_file"] = {
Name = [[test_file]],
Synopsis = [[test a file to see what it is]],
Usage = [[local df = require "lib/dtutils.file"
local result = df.check_if_bin_exists(bin)
bin - string - the binary to check for]],
Description = [[check_if_bin_exists checks to see if the specified binary executable is
in the path.]],
Return_Value = [[result - boolean - true if the executable was found, false if not]],
local result = df.test_file(path, test)
path - string - the path to check
test - one of d, e, f, x where
d - directory
e - exists
f - file
x - executable]],
Description = [[test_file checks a path to see if it is a directory]],
Return_Value = [[result - boolean - true if path is a directory, nil if not]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
@@ -50,10 +92,303 @@ dtutils_file.libdoc.functions["check_if_bin_exists"] = {
Copyright = [[]],
}
function dtutils_file.check_if_bin_exists(bin)
local result = os.execute("which " .. bin)
function dtutils_file.test_file(path, test)
local cmd = "test -"
local engine = os.execute
local cmdstring = ""
if dt.configuration.running_os == "windows" then
cmd = "if exist "
engine = _win_os_execute
end
if test == "d" then
-- test if directory
if dt.configuration.running_os == "windows" then
cmdstring = cmd .. dtutils_file.sanitize_filename(path .. "\\*") .. " echo true"
else
cmdstring = cmd .. test .. " " .. dtutils_file.sanitize_filename(path)
end
elseif test == "e" then
-- test exists
if dt.configuration.running_os == "windows" then
cmdstring = cmd .. dtutils_file.sanitize_filename(path) .. " echo true"
else
cmdstring = cmd .. test .. " " .. dtutils_file.sanitize_filename(path)
end
elseif test == "f" then
-- test if file
if dt.configuration.running_os == "windows" then
if not dtutils_file.test_file(path, "d") then -- make sure it's not a directory
cmdstring = cmd .. dtutils_file.sanitize_filename(path) .. " echo true"
else
return false
end
else
cmdstring = cmd .. test .. " " .. dtutils_file.sanitize_filename(path)
end
elseif test == "x" then
-- test executable
if dt.configuration.running_os == "windows" then
return _is_windows_executable(path)
else
cmdstring = cmd .. test .. " " .. dtutils_file.sanitize_filename(path)
end
else
dt.print_error("[test_file] unknown test " .. test)
return false
end
return engine(cmdstring)
end
--[[
local function to return a case insensitive pattern for matching
i.e. gimp becomes [Gg][Ii][Mm][Pp] which should match any capitalization
of gimp.
]]
local function _case_insensitive_pattern(pattern)
return pattern:gsub("(.)", function(letter)
return string.format("[%s$s]", letter:lower(), letter:upper())
end)
end
--[[
local function to search windows for an executable
]]
local function _search_for_bin_windows(bin)
local result = false
-- use where on path
-- use where on program files
-- use where on program files (x86)
local args = {"", '/R "C:\\Program Files"', '/R "C:\\Program Files (x86)"'}
for _,arg in ipairs(args) do
local cmd = "where " .. arg .. " " .. ds.sanitize(bin)
local p = io.popen(cmd)
local output = p:read("*a")
p:close()
local lines = du.split(output, "\n")
local cibin = _case_insensitive_pattern(bin)
for _,line in ipairs(lines) do
if string.match(line, cibin) then
dt.print_log("found win search match " .. line)
if dtutils_file.test_file(line, "f") and dtutils_file.test_file(line, "x") then
dtutils_file.set_executable_path_preference(bin, line) -- save it so we don't have to search again
return line
end
end
end
end
return result
end
--[[
local function to search *nix systems for an executable
]]
local function _search_for_bin_nix(bin)
local result = false
local p = io.popen("which " .. bin)
local output = p:read("*a")
p:close()
if string.len(output) > 0 then
local spath = dtutils_file.sanitize_filename(output:sub(1, -2))
if dtutils_file.test_file(spath, "f") and dtutils_file.test_file(spath, "x") then
dtutils_file.set_executable_path_preference(bin, spath)
result = spath
end
end
return result
end
--[[
local function to search macos systems for an executable
]]
local function _search_for_bin_macos(bin)
local result = false
result = _search_for_bin_nix(bin) -- see if it's in the path
if not result then
result = false
local search_start = "/Applications"
if dtutils_file.check_if_file_exists("/Applications/" .. bin .. ".app") then
search_start = "/Applications/" .. bin .. ".app"
end
local p = io.popen("find " .. search_start .. " -type f -name " .. bin .. " -print")
local output = p:read("*a")
p:close()
local lines = du.split(output, "\n")
for _,line in ipairs(lines) do
local spath = dtutils_file.sanitize_filename(line:sub(1, -1))
if dtutils_file.test_file(spath, "x") then
dtutils_file.set_executable_path_preference(bin, spath) -- save it so we don't have to search again
result = spath
end
end
end
return result
end
--[[
local function to provide a generic search call that can be
split into operating system specific calls
]]
local function _search_for_bin(bin)
local result = false
if dt.configuration.running_os == "windows" then
result = _search_for_bin_windows(bin)
if result then
result = dtutils_file.sanitize_filename(result)
end
elseif dt.configuration.running_os == "macos" then
result = _search_for_bin_macos(bin)
else
result = _search_for_bin_nix(bin)
end
return result
end
--[[
local function to check if an executable path is
a windows executable on linux or macos, thus requiring wine to run
]]
local function _check_path_for_wine_bin(path)
local result = false
if string.len(path) > 0 then
-- check for windows executable to run under wine
if _is_windows_executable(path) then
if dtutils_file.check_if_file_exists(path) then
result = "wine " .. dtutils_file.sanitize_filename(path)
end
end
end
return result
end
--[[
local function to check if an executable path is
a valid executable. Some generic checks are done before
system specific checks are done.
]]
local function _check_path_for_bin(bin)
local result = false
local path = nil
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
if string.match(bin, PS) then
path = bin
else
path = dtutils_file.get_executable_path_preference(bin)
-- reset path preference is the returned preference is a directory
if dtutils_file.test_file(path, "d") then
dtutils_file.set_executable_path_preference(bin, "")
path = nil
end
end
if path and dtutils_file.test_file(path, "d") then
path = nil
end
if path and dt.configuration.running_os ~= "windows" then
result = _check_path_for_wine_bin(path)
end
if path and not result then
if dtutils_file.test_file(path, "x") then
result = dtutils_file.sanitize_filename(path)
end
end
return result
end
--[[
local function to the old check_if_bin_exists functionality
on windows in order to decrease the amount of windows being
created and destroyed by system calls.
]]
local function _old_check_if_bin_exists(bin) -- only run on windows if preference checked
local result = false
local path = nil
if string.match(bin, "\\") then
path = bin
else
path = dtutils_file.get_executable_path_preference(bin)
end
if string.len(path) > 0 then
if dtutils_file.check_if_file_exists(path) then
if (string.match(path, ".exe$") or string.match(path, ".EXE$")) then
result = dtutils_file.sanitize_filename(path)
end
end
end
return result
end
dtutils_file.libdoc.functions["check_if_bin_exists"] = {
Name = [[check_if_bin_exists]],
Synopsis = [[check if an executable exists]],
Usage = [[local df = require "lib/dtutils.file"
local result = df.check_if_bin_exists(bin)
bin - string - the binary to check for]],
Description = [[check_if_bin_exists checks to see if the specified binary exists.
check_if_bin_exists first checks to see if a preference for the binary has been
registered and uses that if found, after it's verified to be an executable and
exist. If no preference exissts, the user's path is checked for the executable.
If the executable is not found in the users path, then a search of the operating
system is conducted to see if the executable can be found.
If an executalble is found, it's verified to exist and be an executable. Once
the executable is verified, the path is saved as a preference to speed up
subsequent checks. The executable path is sanitized and returned.
If no executable is found, false is returned.]],
Return_Value = [[result - string - the sanitized path of the binary, false if not found]],
Limitations = [[If more than one executable that satisfies the search results is found, the
wrong one may be returned. If the wrong value is returned, the user can still specify the
correct execuable using tools/executable_manager. Most packages are well behaved with the
notiable exception being GIMP on windows. Depending on the packager there are multiple
gimp executables, often with version numbers. In this case, the user needs to specify
the location of the correct executable using executable_manager.]],
Example = [[]],
See_Also = [[executable_manager]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils_file.check_if_bin_exists(bin)
local result = false
if dt.configuration.running_os == "windows" and dt.preferences.read("dtutils.file", "use_old_check_if_bin_exists", "bool") then
result = _old_check_if_bin_exists(bin)
else
result = _check_path_for_bin(bin)
if not result then
result = _search_for_bin(bin)
end
end
return result
end
@@ -79,9 +414,14 @@ dtutils_file.libdoc.functions["split_filepath"] = {
function dtutils_file.split_filepath(str)
-- strip out single quotes from quoted pathnames
str = string.gsub(str, "'", "")
str = string.gsub(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, "(.-)(([^\\/]-)%.?([^%.\\/]*))$")
if result["basename"] == "" and result["filetype"]:len() > 1 then
result["basename"] = result["filetype"]
result["filetype"] = ""
end
return result
end
@@ -194,10 +534,34 @@ dtutils_file.libdoc.functions["check_if_file_exists"] = {
}
function dtutils_file.check_if_file_exists(filepath)
local result = os.execute("test -e " .. filepath)
local result = false
if (dt.configuration.running_os == 'windows') then
filepath = string.gsub(filepath, '[\\/]+', '\\')
local p = io.popen("if exist " .. dtutils_file.sanitize_filename(filepath) .. " (echo 'yes') else (echo 'no')")
local ans = p:read("*all")
p:close()
if string.match(ans, "yes") then
result = true
end
-- result = os.execute('if exist "'..filepath..'" (cmd /c exit 0) else (cmd /c exit 1)')
-- if not result then
-- result = false
-- end
elseif (dt.configuration.running_os == "linux") then
result = os.execute('test -e ' .. dtutils_file.sanitize_filename(filepath))
if not result then
result = false
end
else
local file = io.open(filepath, "r")
if file then
result = true
file:close()
else
result = false
end
end
return result
end
@@ -220,7 +584,11 @@ dtutils_file.libdoc.functions["chop_filetype"] = {
function dtutils_file.chop_filetype(path)
local length = dtutils_file.get_filetype(path):len() + 2
if length > 2 then
return string.sub(path, 1, -length)
else
return path
end
end
dtutils_file.libdoc.functions["file_copy"] = {
@@ -245,9 +613,12 @@ dtutils_file.libdoc.functions["file_copy"] = {
function dtutils_file.file_copy(fromFile, toFile)
local result = nil
-- if cp exists, use it
if dtutils_file.check_if_bin_exists("cp") then
if dt.configuration.running_os == "windows" then
result = os.execute('copy "' .. fromFile .. '" "' .. toFile .. '"')
elseif dtutils_file.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')
@@ -324,7 +695,7 @@ dtutils_file.libdoc.functions["filename_increment"] = {
"01" is added to the basename. If the filename already has an increment, then
1 is added to it and the filename returned.]],
Return_Value = [[result - string - the incremented filename]],
Limitations = [[]],
Limitations = [[The filename will be incremented to 99]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
@@ -347,6 +718,9 @@ function dtutils_file.filename_increment(filepath)
if string.len(increment) > 2 then
-- we got the filename so set the increment to 01
increment = "01"
elseif increment == "99" then
dt.print_error("not incrementing, filename has already been incremented 99 times.")
return filepath
else
increment = string.format("%02d", tonumber(increment) + 1)
basename = string.gsub(basename, "_(%d-)$", "")
@@ -384,11 +758,230 @@ function dtutils_file.create_unique_filename(filepath)
while dtutils_file.check_if_file_exists(filepath) do
filepath = dtutils_file.filename_increment(filepath)
-- limit to 99 more exports of the original export
if string.match(dtfileutils.get_basename(filepath), "_(d-)$") == "99" then
local increment = string.match(dtutils_file.get_basename(filepath), "_(%d-)$")
if increment == "99" then
break
end
end
return filepath
end
dtutils_file.libdoc.functions["set_executable_path_preference"] = {
Name = [[set_executable_path_preference]],
Synopsis = [[set a preference for the path to an executable]],
Usage = [[local df = require "lib/dtutils.file"
df.set_executable_path_preference(executable, path)
executable - string - the name of the executable to set the path for
path - string - the path to the binary]],
Description = [[set_executable_path_preference takes an executable name and path to the
executable and registers the preference for later use.]],
Return_Value = [[]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils_file.set_executable_path_preference(executable, path)
dt.preferences.write("executable_paths", executable, "string", path)
end
dtutils_file.libdoc.functions["get_executable_path_preference"] = {
Name = [[get_executable_path_preference]],
Synopsis = [[return the path to an executable from a preference]],
Usage = [[local df = require "lib/dtutils.file"
local result = df.get_executable_path_preference(executable)
executable - string - the name of the executable to get the path for]],
Description = [[get_executable_path_preference returns the path preference to
the requested executable.]],
Return_Value = [[result - string - path to the executable]],
Limitations = [[executable should be the basename of the executable without extensions]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils_file.get_executable_path_preference(executable)
return dt.preferences.read("executable_paths", executable, "string")
end
dtutils_file.libdoc.functions["executable_path_widget"] = {
Name = [[executable_path_widget]],
Synopsis = [[create a widget to get executable path preferences]],
Usage = [[local df = require "lib/dtutils.file"
local widget = df.executable_path_widget(executables)
executables - table - a table of strings that are executable names]],
Description = [[executable_path_widget takes a table of executable names
and builds a set of file selector widgets to get the path to the executable.
The resulting widgets are wrapped in a box widget and returned.]],
Return_Value = [[widget - widget - a widget containing a file selector widget for
each executable.]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils_file.executable_path_widget(executables)
local box_widgets = {}
table.insert(box_widgets, dt.new_widget("section_label"){label = "select executable(s)"})
for _, executable in pairs(executables) do
table.insert(box_widgets, dt.new_widget("label"){label = "select " .. executable .. " executable"})
local path = dtutils_file.get_executable_path_preference(executable)
if not path then
path = ""
end
table.insert(box_widgets, dt.new_widget("file_chooser_button"){
title = "select " .. executable .. " executable",
value = path,
is_directory = false,
changed_callback = function(self)
if dtutils_file.check_if_bin_exists(self.value) then
dtutils_file.set_executable_path_preference(executable, self.value)
end
end
}
)
end
local box = dt.new_widget("box"){
orientation = "vertical",
table.unpack(box_widgets)
}
return box
end
dtutils_file.libdoc.functions["sanitize_filename"] = {
Name = [[sanitize_filename]],
Synopsis = [[make a filename safe to pass as an argument]],
Usage = [[local df = require "lib/dtutils.file"
local sanitized_filename = df.sanitize_filename(filename)
filename - string - a filepath and filename]],
Description = [[sanitize_file places quotes around the filename in an
operating system specific manner. The result is safe to pass as
an argument to the operating system.]],
Return_Value = [[sanitized_filename - string - quoted filename]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils_file.sanitize_filename(filename)
return ds.sanitize(filename)
end
dtutils_file.libdoc.functions["mkdir"] = {
Name = [[mkdir]],
Synopsis = [[create the directory(ies) if they do not already exists]],
Usage = [[local df = require "lib/dtutils.file"
df.mkdir(path)
path - string - a directory path]],
Description = [[mkdir creates directories if not already exists. It
create whole parents subtree if needed
]],
Return_Value = [[path - string - a directory path]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils_file.mkdir(path)
if not dtutils_file.check_if_file_exists(path) then
local mkdir_cmd = dt.configuration.running_os == "windows" and "mkdir" or "mkdir -p"
return dsys.external_command(mkdir_cmd.." "..path)
else
return 0
end
end
dtutils_file.libdoc.functions["rmdir"] = {
Name = [[rmdir]],
Synopsis = [[recursively remove a directory]],
Usage = [[local df = require "lib/dtutils.file"
df.rmdir(path)
path - string - a directory path]],
Description = [[rm allow to recursively remove directories]],
Return_Value = [[path - string - a directory path]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils_file.rmdir(path)
local rm_cmd = dt.configuration.running_os == "windows" and "rmdir /S /Q" or "rm -r"
return dsys.external_command(rm_cmd.." "..path)
end
dtutils_file.libdoc.functions["create_tmp_file"] = {
Name = [[create_tmp_file]],
Synopsis = [[creates a temporary file]],
Usage = [[local df = require "lib/dtutils.file
local result = df.create_tmp_file()]],
Description = [[create_tmp_file can be used to create temporary files]],
Return_Value = [[result - string - path to the created temporary file.]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils_file.create_tmp_file()
local tmp_file = os.tmpname()
if dt.configuration.running_os == "windows" then
tmp_file = dt.configuration.tmp_dir .. tmp_file -- windows os.tmpname() defaults to root directory
end
local f = io.open(tmp_file, "w")
if not f then
log.msg(log.error, string.format("Error writing to `%s`", tmp_file))
os.remove(tmp_file)
return nil
end
return tmp_file
end
--[[
The new check_if_bin_exists() does multiple calls to the operating system to check
if the file exists and is an executable. On windows, each call to the operating system
causes a window to open in order to run the command, then the window closes when the
command exits. If the user gets annoyed by the "flickering windows", then they can
enable this preference to use the old check_if_bin_exists() that relys on the
executable path preferences and doesn't do as many checks.
]]
if dt.configuration.running_os == "windows" then
dt.preferences.register("dtutils.file", "use_old_check_if_bin_exists", "bool",
"lua scripts use old check_if_bin_exists()",
"lessen flickering windows effect when scripts run",
false)
end
return dtutils_file

View File

@@ -48,8 +48,7 @@ dtutils_log.libdoc = {
local dt = require "darktable"
local dt_print_error = dt.print_error
-- work around until dt.print_log()
local dt_print_log = dt.print_error
local dt_print_log = dt.print_log
local dt_print = dt.print
-- set the default log levels
@@ -243,6 +242,7 @@ function dtutils_log.log_level(...)
for _,v in ipairs(levels) do
if dtutils_log[v].enabled == true then
log_level = dtutils_log[v]
break
end
end
end

View File

@@ -1,4 +1,7 @@
local dtutils_string = {}
local dt = require "darktable"
dtutils_string.libdoc = {
Name = [[dtutils.string]],
Synopsis = [[a library of string utilities for use in darktable lua scripts]],
@@ -164,10 +167,107 @@ dtutils_string.libdoc.functions["urlencode"] = {
function dtutils_string.urlencode(str)
if (str) then
str = string.gsub (str, "\n", "\r\n")
str = string.gsub (str, "([^%w ])", function () return string.format ("%%%02X", string.byte()) end)
str = string.gsub (str, "([^%w ])", function (c) return string.format ("%%%02X", string.byte(c)) end)
str = string.gsub (str, " ", "+")
end
return str
end
dtutils_string.libdoc.functions["is_not_sanitized"] = {
Name = [[is_not_sanitized]],
Synopsis = [[Check if a string has been sanitized]],
Usage = [[local ds = require "lib/dtutils.string"
local result = ds.is_not_sanitized(str)
str - string - the string that needs to be made safe]],
Description = [[is_not_sanitized checks a string to see if it
has been made safe use passing as an argument in a system command.]],
Return_Value = [[result - boolean - true if the string is not sanitized otherwise false]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
local function _is_not_sanitized_posix(str)
-- A sanitized string must be quoted.
if not string.match(str, "^'.*'$") then
return true
-- A quoted string containing no quote characters within is sanitized.
elseif string.match(str, "^'[^']*'$") then
return false
end
-- Any quote characters within a sanitized string must be properly
-- escaped.
local quotesStripped = string.sub(str, 2, -2)
local escapedQuotesRemoved = string.gsub(quotesStripped, "'\\''", "")
if string.find(escapedQuotesRemoved, "'") then
return true
else
return false
end
end
local function _is_not_sanitized_windows(str)
if not string.match(str, "^\".*\"$") then
return true
else
return false
end
end
function dtutils_string.is_not_sanitized(str)
if dt.configuration.running_os == "windows" then
return _is_not_sanitized_windows(str)
else
return _is_not_sanitized_posix(str)
end
end
dtutils_string.libdoc.functions["sanitize"] = {
Name = [[sanitize]],
Synopsis = [[surround a string in quotes making it safe to pass as an argument]],
Usage = [[local ds = require "lib/dtutils.string"
local result = ds.sanitize(str)
str - string - the string that needs to be made safe]],
Description = [[sanitize converts a string into a version suitable for
use passing as an argument in a system command.]],
Return_Value = [[result - string - a websafe string]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
local function _sanitize_posix(str)
if _is_not_sanitized_posix(str) then
return "'" .. string.gsub(str, "'", "'\\''") .. "'"
else
return str
end
end
local function _sanitize_windows(str)
if _is_not_sanitized_windows(str) then
return "\"" .. string.gsub(str, "\"", "\"^\"\"") .. "\""
else
return str
end
end
function dtutils_string.sanitize(str)
if dt.configuration.running_os == "windows" then
return _sanitize_windows(str)
else
return _sanitize_posix(str)
end
end
return dtutils_string

133
lib/dtutils/system.lua Normal file
View File

@@ -0,0 +1,133 @@
local dtutils_system = {}
local dt = require "darktable"
require "official/yield" -- necessary for dt.control.execute()
dtutils_system.libdoc = {
Name = [[dtutils.system]],
Synopsis = [[a library of system utilities for use in darktable lua scripts]],
Usage = [[local ds = require "lib/dtutils.system"]],
Description = [[This library contains routines for interfacing to the operating system from
darktable lua scripts.]],
Return_Value = [[du - library - the darktable lua system library]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.]],
Copyright = [[Copyright (c) 2018 Bill Ferguson <wpferguson@gmail.com>]],
functions = {}
}
dtutils_system.libdoc.functions["external_command"] = {
Name = [[external_command]],
Synopsis = [[pass a command to the operating system for execution and return the result]],
Usage = [[local dsys = require "lib/dtutils.system"
local result = dsys.external_command(command)
command - string - a string containing the command and arguments to be passed to the operating system for execution.]],
Description = [[external_command passes a command to the operating system for execution and returns the results.]],
Return_Value = [[result - the return value signalling success or failure.]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils_system.external_command(command)
local result = nil
if dt.configuration.running_os == "windows" then
result = dtutils_system.windows_command(command)
else
result = dt.control.execute(command)
end
return result
end
dtutils_system.libdoc.functions["windows_command"] = {
Name = [[windows_command]],
Synopsis = [[pass a command to the windows operating system for execution and return the result]],
Usage = [[local dsys = require "lib/dtutils.system"
local result = dsys.windows_command(command)
command - string - a string containing the command and arguments to be passed to the operating system for execution.]],
Description = [[The normal method of executing an operating system command is using dt.control.execute(), but that doesn't
work with Windows when more than one item in the command is quoted. In order to ensure command execution on Windows we
create a batch file in the temporary directory, put the command in it, execute the batch file, then return the result.]],
Return_Value = [[result - the return value signalling success or failure.]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils_system.windows_command(command)
local result = 1
local fname = dt.configuration.tmp_dir .. "/run_command.bat"
local file = io.open(fname, "w")
if file then
dt.print_log("opened file")
command = string.gsub(command, "%%", "%%%%") -- escape % from windows shell
file:write(command)
file:close()
result = dt.control.execute(fname)
dt.print_log("result from windows command was " .. result)
os.remove(fname)
else
dt.print_error("Windows command failed: unable to create batch file")
end
return result
end
dtutils_system.libdoc.functions["launch_default_app"] = {
Name = [[launch_default_app]],
Synopsis = [[open file in default application]],
Usage = [[local dsys = require "lib/dtutils.file"
result = dsys.launch_default_app(path)
path - string - a file path]],
Description = [[launch_default_app allows opening a file in the application that is assigned as default
for that filetype in the users's system]],
Return_Value = [[result - the return value signalling success or failure.]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils_system.launch_default_app(path)
local open_cmd = "xdg-open "
if (dt.configuration.running_os == "windows") then
open_cmd = "" -- On Windows we don't need any command. (start e.g. has problems with spaces in the filename, even if we put quoter around them.) https://stackoverflow.com/questions/13691827/opening-file-with-spaces-in-windows-via-command-prompt
elseif (dt.configuration.running_os == "macos") then
open_cmd = "open "
end
return dtutils_system.external_command(open_cmd .. path)
end
return dtutils_system

View File

@@ -0,0 +1,39 @@
# OpenInExplorer Darktable Plug-In.
# Copyright (C) 2020 Volker Lenhardt
# This file is distributed under the same license as the PACKAGE package.
# Volker Lenhardt <volker.lenhardt@unitybox.de>, 2020.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: OpenInExplorer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-19 14:43+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Volker Lenhardt <volker.lenhardt@unitybox.de>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: OpenInExplorer.lua:64
msgid ""
"OpenInExplorer plug-in only supports Linux, macOS, and Windows at this time"
msgstr "OpenInExplorer-Plug-in kann nur auf macOS, Linux oder Windows laufen"
#: OpenInExplorer.lua:79
msgid "Please select an image"
msgstr "Es wurde kein Bild ausgewählt"
#: OpenInExplorer.lua:98
msgid "Please select fewer images (max 15)"
msgstr "Bitte nicht mehr als 15 Bilder auswählen"
#: OpenInExplorer.lua:105
msgid "show in file explorer"
msgstr "im Dateimanager anzeigen"
#: OpenInExplorer.lua:107
msgid "Opens the file manager at the selected image's location"
msgstr "Öffnet den Dateimanager an der Position des ausgewählten Bildes"

View File

@@ -8,14 +8,14 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-01-31 22:59+0100\n"
"PO-Revision-Date: 2016-01-31 23:09+0100\n"
"PO-Revision-Date: 2018-09-18 10:22+0200\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Last-Translator: Tobias Jakobs <tobias.jakobs@gmail.com>\n"
"Last-Translator: Simon Legner <Simon.Legner@gmail.com>\n"
"Language-Team: \n"
"X-Generator: Poedit 1.8.4\n"
"X-Generator: Poedit 2.1.1\n"
#: hugin.lua:73
msgid "hugin not found"
@@ -27,7 +27,7 @@ msgstr "Versuche Bilder zusammenzufügen"
#: hugin.lua:102
msgid "Creating pto file"
msgstr "Erzeuge pto Datei"
msgstr "Erzeuge pto-Datei"
#: hugin.lua:105
msgid "Running Assistent"
@@ -39,4 +39,4 @@ msgstr "Aufruf von hugin fehlgeschlagen ..."
#: hugin.lua:124
msgid "Hugin Panorama"
msgstr "Hugin Panorama"
msgstr "Hugin-Panorama"

View File

@@ -2,45 +2,45 @@ msgid ""
msgstr ""
"Project-Id-Version: Lua gettextTest\n"
"POT-Creation-Date: 2016-01-22 15:33+0100\n"
"PO-Revision-Date: 2016-01-22 23:28+0100\n"
"Last-Translator: Tobias Jakobs <tobias.jakobs@gmail.com>\n"
"PO-Revision-Date: 2018-09-18 10:21+0200\n"
"Last-Translator: Simon Legner <Simon.Legner@gmail.com>\n"
"Language-Team: \n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.4\n"
"X-Generator: Poedit 2.1.1\n"
"X-Poedit-Basepath: .\n"
"Plural-Forms: nplurals=2;t" plural=(n != 1);\n"
"Plural-Forms: nplurals=2;t\" plural=(n != 1);\n"
"X-Poedit-KeywordsList: gettext;dgettext:2;dcgettext:2;ngettext:1,2;dngettext:2,3\n"
"X-Poedit-SearchPath-0: .\n"
msgid "KML/KMZ Export"
msgstr "KML/KMZ Expor
msgstr "KML/KMZ-Export"
msgid "Export Image %i/%i"
msgstr "Exportiere Bild %i/%i"
msgid "KML export: Open KML/KMZ file after export"
msgstr "KML export: KML/KMZ-Datei nach dem exportieren öffnen"
msgstr "KML-Export KML/KMZ-Datei nach dem Export öffnen"
msgid "Opens the KML file after the export with the standard programm for KML files"
msgstr "Öffnet die KML-Datei mit dem Standardprogram für KML-Dateien"
msgid "KML export: Export directory"
msgstr "KML export: Export-Verzeichnis"
msgstr "KML-Export: Export-Verzeichnis"
msgid "A directory that will be used to export the KML/KMZ files"
msgstr "Verzeichnis in das die KML/KMZ-Dateien exportiert werden"
msgid "KML export: Connect images with path"
msgstr "KML export: Bilder mit einer Linie verbinden"
msgstr "KML-Export: Bilder mit einer Linie verbinden"
msgid "connect all images with a path"
msgstr "Verbinde alle Bilder mit einer Linie"
msgid "KML export: Create KMZ file"
msgstr "KML export: KMZ-Datei erstellen"
msgstr "KML-Export: KMZ-Datei erstellen"
msgid "Compress all imeges to one KMZ file"
msgstr "Alle Bilder in einer KMZ-Datei komprimieren"

View File

@@ -0,0 +1,122 @@
# Italian translations for ext_editor package.
# Copyright (C) 2019 THE ext_editor'S COPYRIGHT HOLDER
# This file is distributed under the same license as the ext_editor package.
# marco <marco.carrarini@gmail.com>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: ext_editor\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-01-01 19:15+0100\n"
"PO-Revision-Date: 2019-12-28 21:45+0100\n"
"Last-Translator: marco <marco.carrarini@gmail.com>\n"
"Language-Team: Italian\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=ASCII\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: ext_editor.lua:143
msgid " editors configured"
msgstr " editor configurati"
#: ext_editor.lua:150
msgid "exporting image "
msgstr "Esportazione immagine "
#: ext_editor.lua:159
msgid "not a valid choice"
msgstr "Scelta non valida"
#: ext_editor.lua:165
msgid "please select one image"
msgstr "Selezionare una immagine"
#: ext_editor.lua:174
msgid " not found"
msgstr " non trovato"
#: ext_editor.lua:195
msgid "file type not allowed"
msgstr "tipo di file non consentito"
#: ext_editor.lua:232
msgid "error copying file "
msgstr "Errore copia file "
#: ext_editor.lua:239
msgid "launching "
msgstr "Lancio "
#: ext_editor.lua:242
msgid "error launching "
msgstr "Errore lancio "
#: ext_editor.lua:341
msgid "error moving file "
msgstr "Errore spostamento file "
#: ext_editor.lua:355
msgid "collection"
msgstr "Collezione"
#: ext_editor.lua:360
msgid "choose program"
msgstr "Scegli programma"
#: ext_editor.lua:361
msgid "select the external editor from the list"
msgstr "Seleziona un editor esterno dalla lista"
#: ext_editor.lua:371
msgid "edit"
msgstr "Modifica"
#: ext_editor.lua:372
msgid "open the selected image in external editor"
msgstr "Apre immagine selezionata in editor esterno"
#: ext_editor.lua:382
msgid "edit a copy"
msgstr "Modifica copia"
#: ext_editor.lua:383
msgid "create a copy of the selected image and open it in external editor"
msgstr "Apre una copia della immagine selezionata in editor esterno"
#: ext_editor.lua:392
msgid "update list"
msgstr "Aggiorna lista"
#: ext_editor.lua:393
msgid "update list of programs if lua preferences are changed"
msgstr "Aggiorna la lista dei programmi se le preferenze di lua sono cambiate"
#: ext_editor.lua:413
msgid "external editors"
msgstr "Editor esterni"
#: ext_editor.lua:434
msgid "executable for external editor "
msgstr "Eseguibile per editor esterno "
#: ext_editor.lua:435
msgid "select executable for external editor"
msgstr "seleziona eseguibile per editor esterno "
#: ext_editor.lua:435
msgid "(None)"
msgstr "(Nessuno)"
#: ext_editor.lua:437
msgid "name of external editor "
msgstr "Nome editor esterno "
#: ext_editor.lua:438
msgid "friendly name of external editor"
msgstr "Nome comune editor esterno"
#: ext_editor.lua:444
msgid "edit with program "
msgstr "Modifica con programma "

View File

@@ -28,10 +28,11 @@ USAGE
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local https = require "ssl.https"
local cjson = require "cjson"
dt.configuration.check_version(...,{2,0,0},{3,0,0},{4,0,0})
du.check_min_api_version("2.0.0", "check_for_updates")
-- compare two version strings of the form "major.minor.patch"
-- returns -1, 0, 1 if the first version is smaller, equal, greater than the second version,

View File

@@ -26,8 +26,9 @@ USAGE
]]
local dt = require "darktable"
local du = require "lib/dtutils"
dt.configuration.check_version(...,{3,0,0},{4,0,0})
du.check_min_api_version("3.0.0", "copy_paste_metadata")
-- set this to "false" if you don't want to overwrite metadata fields
-- (title, description, creator, publisher and rights) that are already set

View File

@@ -1,6 +1,6 @@
--[[
This file is part of darktable,
copyright (c) 2014 Tobias Ellinghaus
copyright (c) 2014--2018 Tobias Ellinghaus
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
]]
--[[
DELETE LONG TAGS
A simple script that will automatically delete all tag longer than a set length
A simple script that will automatically delete all tags longer than a set length
USAGE
* require this script from your main lua file
@@ -29,7 +29,9 @@ all tags longer than the given length will be automatically deleted at every res
]]
local dt = require "darktable"
dt.configuration.check_version(...,{2,0,0},{3,0,0},{4,0,0})
local du = require "lib/dtutils"
du.check_min_api_version("2.0.0", "delete_long_tags")
dt.preferences.register("delete_long_tags", "length", "integer",
"maximum length of tags to keep",
@@ -38,10 +40,18 @@ dt.preferences.register("delete_long_tags", "length", "integer",
local max_length = dt.preferences.read("delete_long_tags", "length", "integer")
-- deleting while iterating the tags list seems to break the iterator!
local long_tags = {}
for _,t in ipairs(dt.tags) do
local len = #t.name
if len > max_length then
print("deleting tag `"..t.name.."' (length: "..len..")")
t:delete()
table.insert(long_tags, t.name)
end
end
for _,name in pairs(long_tags) do
tag = dt.tags.find(name)
tag:delete()
end

View File

@@ -0,0 +1,47 @@
--[[
This file is part of darktable,
copyright (c) 2018 Tobias Ellinghaus
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[
DELETE UNUSED TAGS
A simple script that will automatically delete all tags that are not attached to any images
USAGE
* require this script from your main lua file
* restart darktable
all tags that are not used will be automatically deleted at every restart
]]
local dt = require "darktable"
local du = require "lib/dtutils"
du.check_min_api_version("5.0.0", "delete_unused_tags")
-- deleting while iterating the tags list seems to break the iterator!
local unused_tags = {}
for _, t in ipairs(dt.tags) do
if #t == 0 then
table.insert(unused_tags, t.name)
end
end
for _,name in pairs(unused_tags) do
print("deleting tag `" .. name .. "'")
tag = dt.tags.find(name)
tag:delete()
end

View File

@@ -33,14 +33,18 @@ TODO
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
require "official/yield"
local dtsys = require "lib/dtutils.system"
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
local gettext = dt.gettext
dt.configuration.check_version(...,{3,0,0},{4,0,0})
du.check_min_api_version("3.0.0", "enfuse")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("enfuse",dt.configuration.config_dir.."/lua/locale/")
gettext.bindtextdomain("enfuse",dt.configuration.config_dir..PS .. "lua" .. PS .. "locale" .. PS)
local function _(msgid)
return gettext.dgettext("enfuse", msgid)
@@ -50,14 +54,39 @@ end
-- is enfuse installed?
local enfuse_installed = df.check_if_bin_exists("enfuse")
if enfuse_installed then
dt.print_log("found enfuse executable at " .. enfuse_installed)
-- instance of DT tiff exporter
local tiff_exporter = dt.new_format("tiff")
tiff_exporter.bpp = 16
tiff_exporter.max_height = 0
tiff_exporter.max_width = 0
-- check the version so that we can use the correct arguments
local version = nil
local p = io.popen(enfuse_installed .. " --version")
local f = p:read("all")
p:close()
version = string.match(f, "enfuse (%d.%d)")
dt.print_log("enfuse version is " .. version)
-- initialize exposure_mu value and depth setting in config to sane defaults (would be 0 otherwise)
if dt.preferences.read("enfuse", "depth", "integer") == 0 then
dt.preferences.write("enfuse", "depth", "integer", 2)
dt.preferences.write("enfuse", "exposure_mu", "float", 0.5)
dt.preferences.write("enfuse", "exposure_optimum", "float", 0.5)
end
-- set up some widgets, initialized from config
local exposure_mu = dt.new_widget("slider")
local exposure_mu = nil
if version < "4.2" then
exposure_mu = dt.new_widget("slider")
{
label = "exposure mu",
tooltip = "center also known as MEAN of Gaussian weighting function (0 <= MEAN <= 1); default: 0.5",
@@ -65,6 +94,16 @@ local exposure_mu = dt.new_widget("slider")
hard_max = 1,
value = dt.preferences.read("enfuse", "exposure_mu", "float")
}
else
exposure_mu = dt.new_widget("slider")
{
label = "exposure optimum",
tooltip = "optimum exposure value, usually the maximum of the weighting function (0 <= OPTIMUM <=1); default 0.5",
hard_min = 0,
hard_max = 1,
value = dt.preferences.read("enfuse", "exposure_optimum", "float")
}
end
local depth = dt.new_widget("combobox")
{
@@ -75,6 +114,129 @@ local depth = dt.new_widget("combobox")
"8", "16", "32"
}
local blend_colorspace = dt.new_widget("combobox")
{
label = "blend colorspace",
tooltip = "Force blending in selected colorspace",
changed_callback = function(w) dt.preferences.write("enfuse", "blend_colorspace", "string", w.selected) end,
"", "identity", "ciecam"
}
local enfuse_button = dt.new_widget("button")
{
label = enfuse_installed and "run enfuse" or "enfuse not installed",
clicked_callback = function ()
-- remember exposure_mu
-- TODO: find a way to save it whenever the value changes
local mu = exposure_mu.value
if version < "4.2" then
dt.preferences.write("enfuse", "exposure-mu", "float", mu)
else
dt.preferences.write("enfuse", "exposure-optimum", "float", mu)
end
-- create a temp response file
local response_file = os.tmpname()
if dt.configuration.running_os == "windows" then
response_file = dt.configuration.tmp_dir .. response_file -- windows os.tmpname() defaults to root directory
end
local f = io.open(response_file, "w")
if not f then
dt.print(string.format(_("Error writing to `%s`"), response_file))
os.remove(response_file)
return
end
-- add all filenames to the response file
local cnt = 0
local n_skipped = 0
local target_dir
for i_, i in ipairs(dt.gui.action_images) do
-- only use ldr files as enfuse can't open raws
if i.is_ldr then
cnt = cnt + 1
f:write(i.path..PS..i.filename.."\n")
target_dir = i.path
-- alternatively raws will be exported as tiff
elseif i.is_raw then
local tmp_exported = os.tmpname()..".tif"
if dt.configuration.running_os == "windows" then
tmp_exported = dt.configuration.tmp_dir .. tmp_exported -- windows os.tmpname() defaults to root directory
end
dt.print(string.format(_("Converting raw file '%s' to tiff..."), i.filename))
tiff_exporter:write_image(i, tmp_exported, false)
dt.print_log(string.format("Raw file '%s' converted to '%s'", i.filename, tmp_exported))
cnt = cnt + 1
f:write(tmp_exported.."\n")
target_dir = i.path
-- other images will be skipped
else
dt.print(string.format(_("Skipping %s..."), i.filename))
n_skipped = n_skipped + 1
end
end
f:close()
-- bail out if there is nothing to do
if cnt == 0 then
dt.print(_("No suitable images selected, nothing to do for enfuse"))
os.remove(response_file)
return
end
if n_skipped > 0 then
dt.print(string.format(_("%d image(s) skipped"), n_skipped))
end
-- call enfuse on the response file
-- TODO: find something nicer
local ugly_decimal_point_hack = string.gsub(string.format("%.04f", mu), ",", ".")
local output_image_date = os.date("%Y%m%d%H%M%S")
local output_image = target_dir.. PS .. "enfuse-"..output_image_date..".tif"
local exposure_option = " --exposure-optimum "
local blend_colorspace_option = ""
if #blend_colorspace.value > 1 then
blend_colorspace_option = " --blend-colorspace="..blend_colorspace.value
end
if version < "4.2" then
exposure_option = " --exposure-mu "
end
local command = enfuse_installed.." --depth "..depth.value..exposure_option..ugly_decimal_point_hack
..blend_colorspace_option
.." -o \""..output_image.."\" \"@"..response_file.."\""
if dtsys.external_command( command) > 0 then
dt.print(_("Enfuse failed, see terminal output for details"))
os.remove(response_file)
return
end
-- remove the response file
os.remove(response_file)
-- import resulting tiff
local image = dt.database.import(output_image)
-- tell the user that everything worked
dt.print(_("enfuse was successful, resulting image has been imported"))
-- normally printing to stdout is bad, but we allow enfuse to show its output, so adding one extra line is ok
print(string.format(_("enfuse: done, resulting image '%s' has been imported with id %d"), output_image, image.id))
end
}
local lib_widgets = {}
if not enfuse_installed then
table.insert(lib_widgets, df.executable_path_widget({"ffmpeg"}))
end
table.insert(lib_widgets, exposure_mu)
table.insert(lib_widgets, depth)
table.insert(lib_widgets, blend_colorspace)
table.insert(lib_widgets, enfuse_button)
-- ... and tell dt about it all
dt.register_lib(
"enfuse", -- plugin name
@@ -86,83 +248,16 @@ dt.register_lib(
{
orientation = "vertical",
sensitive = enfuse_installed,
exposure_mu,
depth,
dt.new_widget("button")
{
label = enfuse_installed and "run enfuse" or "enfuse not installed",
clicked_callback = function (_)
-- remember exposure_mu
-- TODO: find a way to save it whenever the value changes
local mu = exposure_mu.value
dt.preferences.write("enfuse", "exposure_mu", "float", mu)
-- create a temp response file
local response_file = os.tmpname()
local f = io.open(response_file, "w")
if not f then
dt.print("error writing to `"..response_file.."'")
os.remove(response_file)
return
end
-- add all filenames to the response file
local cnt = 0
local n_skipped = 0
local target_dir
for _, i in ipairs(dt.gui.action_images) do
-- only use ldr files as enfuse can't open raws. alternatively we could export raws that we encounter
if i.is_ldr then
cnt = cnt + 1
f:write(i.path.."/"..i.filename.."\n")
target_dir = i.path
else
dt.print("skipping "..i.filename)
n_skipped = n_skipped + 1
end
end
f:close()
-- bail out if there is nothing to do
if cnt == 0 then
dt.print("no suitable images selected, nothing to do for enfuse")
os.remove(response_file)
return
end
if n_skipped == 1 then
dt.print(n_skipped.." image skipped")
elseif n_skipped > 1 then
dt.print(n_skipped.." images skipped")
end
-- call enfuse on the response file
-- TODO: find something nicer
local ugly_decimal_point_hack = string.gsub(string.format("%.04f", mu), ",", ".")
-- TODO: make filename unique
local output_image = target_dir.."/enfuse.tiff"
local command = "enfuse --depth "..depth.value.." --exposure-mu "..ugly_decimal_point_hack
.." -o \""..output_image.."\" \"@"..response_file.."\""
if dt.control.execute( command) > 0 then
dt.print("enfuse failed, see terminal output for details")
os.remove(response_file)
return
end
-- remove the response file
os.remove(response_file)
-- import resulting tiff
local image = dt.database.import(output_image)
-- tell the user that everything worked
dt.print("enfuse was successful, resulting image was imported")
-- normally printing to stdout is bad, but we allow enfuse to show its output, so adding one extra line is ok
print("enfuse: done, resulting image '"..output_image.."' was imported with id "..image.id)
end
}
table.unpack(lib_widgets)
},
nil,-- view_enter
nil -- view_leave
)
else
dt.print_error("enfuse executable not found")
error("enfuse executable not found")
dt.print(_("Could not find enfuse executable. Not loading enfuse exporter..."))
end
-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua
-- kate: hl Lua;

View File

@@ -35,10 +35,11 @@ USAGE
-- * make filenames with double quotes (") work
local dt = require "darktable"
local du = require "lib/dtutils"
require "darktable.debug"
require "official/yield"
dt.configuration.check_version(...,{2,1,0},{3,0,0},{4,0,0})
du.check_min_api_version("2.1.0", "generate_image_txt")
dt.preferences.register("generate_image_txt",
"enabled",

View File

@@ -29,7 +29,9 @@ This plugin will add a widget at the bottom of the left column in lighttable mod
]]
local dt = require "darktable"
dt.configuration.check_version(...,{2,0,0},{3,0,0},{4,0,0})
local du = require "lib/dtutils"
du.check_min_api_version("2.0.0", "image_path_in_ui")
local main_label = dt.new_widget("label"){selectable = true, ellipsize = "middle", halign = "start"}

View File

@@ -67,7 +67,7 @@ end)
-- add a new global function to register import filters
dt.register_import_filter = function(name, callback)
local active_filter = dt.preferences.read("import_filter_manager", "active_filter", "string")
dt.print_error("registering import filter `" .. name .. "'")
dt.print_log("registering import filter `" .. name .. "'")
import_filter_list[name] = callback
n_import_filters = n_import_filters + 1
filter_dropdown[n_import_filters] = name

View File

@@ -34,7 +34,9 @@ increase it if you need more temporary selection buffers
]]
local dt = require "darktable"
dt.configuration.check_version(...,{2,0,0},{3,0,0},{4,0,0})
local du = require "lib/dtutils"
du.check_min_api_version("2.0.0", "save_selection")
local buffer_count = 5

View File

@@ -34,9 +34,10 @@ Plugin allows you to choose how many thumbnails you need per row
]]
local dt = require "darktable"
local du = require "lib/dtutils"
require "official/yield"
dt.configuration.check_version(...,{2,0,0},{3,0,0},{4,0,0})
du.check_min_api_version("2.0.0", "selection_to_pdf")
dt.preferences.register
("selection_to_pdf","Open with","string",

View File

@@ -0,0 +1,192 @@
--[[
This file is part of darktable,
copyright (c) 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 <http://www.gnu.org/licenses/>.
]]
--[[
executable_manager.lua - a tool for managing external executables used by darktable lua scripts
executable_manager is a tool for managing the executable preferences stored in the darktablerc file.
On startup the darktablerc file is scanned and a widget is built for each executable path. The user
can select the executable from a drop down list and then modify the settings as desired.
Any changes made using executable_manager won't be saved in the darktablerc file until darktable exits, but
the preference is updated when the change is made so scripts will pick up the changes without restarting
darktable.
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
du.check_min_api_version("5.0.0", "executable_manager")
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
local gettext = dt.gettext
gettext.bindtextdomain("executable_manager",dt.configuration.config_dir.."/lua/locale/")
-- - - - - - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - - - - - -
local function _(msg)
return gettext.dgettext("executable_manager", msg)
end
local function grep(file, pattern)
local result = {}
if dt.configuration.running_os == "windows" then
-- use find to get the matches
local command = "\\windows\\system32\\find.exe " .. "\"" .. pattern .. "\"" .. " " .. file
local f = io.popen(command)
local output = f:read("all")
f:close()
-- strip out the first line
result = du.split(output, "\n")
table.remove(result, 1)
else
-- use grep and just return the answers
local command = "grep " .. pattern .. " " .. file
local f = io.popen(command)
local output = f:read("all")
f:close()
result = du.split(output, "\n")
end
return result
end
local function update_combobox_choices(combobox, choice_table, selected)
local items = #combobox
local choices = #choice_table
for i, name in ipairs(choice_table) do
combobox[i] = name
end
if choices < items then
for j = items, choices + 1, -1 do
combobox[j] = nil
end
end
combobox.value = selected
end
-- - - - - - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N P R O G R A M
-- - - - - - - - - - - - - - - - - - - - - - - - - - - -
local exec_man = {} -- our own namespace
local DARKTABLERC = dt.configuration.config_dir .. PS .. "darktablerc"
-- grep the executable_paths statements out of the darktablerc file
local matches = grep(DARKTABLERC, "executable_paths")
-- check if we have something to manage and exit if not
if #matches == 0 then
dt.print(_("No executable paths found, exiting..."))
return
end
-- build a table of the path preferences
local exec_table = {}
for _,pref in ipairs(matches) do
local parts = du.split(pref, "=")
local tmp = du.split(parts[1], "/") -- preferences are stored with forward slashes
table.insert(exec_table, tmp[#tmp])
end
local executable_path_widgets = {}
for i,exec in ipairs(exec_table) do
executable_path_widgets[exec] = dt.new_widget("file_chooser_button"){
title = _("select ") .. exec .. _(" executable"),
value = df.get_executable_path_preference(exec),
is_directory = false,
changed_callback = function(self)
if df.check_if_bin_exists(self.value) then
df.set_executable_path_preference(exec, self.value)
end
end
}
end
-- create a stack widget to hold the executable path widgets
exec_man.stack = dt.new_widget("stack"){}
-- create a combobox to for indexing into the stack of widgets
exec_man.selector = dt.new_widget("combobox"){
label = "executable",
tooltip = _("select executable to modify"),
value = 1, "placeholder",
changed_callback = function(self)
for i,exec in ipairs(exec_table) do
if self.value == exec then
exec_man.stack.active = i
end
end
end
}
-- loop through the tabke of path preferences and populate the widgets
for i,exec in ipairs(exec_table) do
exec_man.stack[i] = dt.new_widget("box"){
executable_path_widgets[exec],
dt.new_widget("button"){
label = "clear",
tooltip = _("Clear path for ") .. exec,
clicked_callback = function()
df.set_executable_path_preference(exec, "")
executable_path_widgets[exec].value = ""
end
}
}
df.executable_path_widget({exec})
end
update_combobox_choices(exec_man.selector, exec_table, 1)
-- register the lib
dt.register_lib(
"executable_manager", -- Module name
"executable manager", -- Visible name
true, -- expandable
false, -- resetable
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_BOTTOM", 100}}, -- containers
dt.new_widget("box") -- widget
{
orientation = "vertical",
exec_man.selector,
exec_man.stack,
},
nil,-- view_enter
nil -- view_leave
)

99
tools/gen_i18n_mo.lua Normal file
View File

@@ -0,0 +1,99 @@
--[[
gen_18n_mo.lua - generate .mo files from .po files and put them in the correct place
Copyright (C) 2016,2018 Bill Ferguson <wpferguson@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
]]
--[[
gen_i18n_mo - generate translation files from the source and place them in the appropriate locale directory
gen_i18n_mo finds all the .po files scattered throughout the script tree, compiles them into
.mo files and places them in the correct locale directory for use by the gettext tools.
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
local log = require "lib/dtutils.log"
local dtsys = require "lib/dtutils.system"
du.check_min_api_version("5.0.0", "gen_I18n_mo")
-- figure out the path separator
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
local LUA_DIR = dt.configuration.config_dir .. PS .. "lua" .. PS
local LOCALE_DIR = dt.configuration.config_dir .. PS .. "lua" .. PS .. "locale" .. PS
-- check if we have msgfmt
local msgfmt_executable = df.check_if_bin_exists("msgfmt")
if msgfmt_executable then
-- find the .po files
local find_cmd = "find -L " .. LUA_DIR .. " -name \\*.po -print"
if dt.configuration.running_os == "windows" then
find_cmd = "dir /b/s " .. LUA_DIR .. "\\*.po"
end
local output = io.popen(find_cmd)
-- for each .po file....
for line in output:lines() do
local fname = df.get_filename(line)
-- get the language used... this depends on the file being named using
-- the convention /..../lang/LC_MESSAGES/file.po where lang is de_DE, fr_FR, etc.
local path_parts = du.split(line, PS)
local lang = path_parts[#path_parts - 2]
-- ensure there is a destination directory for them
local mkdir_cmd = "mkdir -p "
if dt.configuration.running_os == "windows" then
mkdir_cmd = "mkdir "
end
if not df.check_if_file_exists(LOCALE_DIR .. lang .. PS .. "LC_MESSAGES") then
log.msg(log.info, "Creating locale", lang)
os.execute(mkdir_cmd .. LOCALE_DIR .. lang .. PS .. "LC_MESSAGES")
end
-- generate the mo file
fname = string.gsub(fname, ".po$", ".mo")
log.msg(log.info, "Compiling translation to", fname)
local result = os.execute(msgfmt_executable .. " -o " .. LOCALE_DIR .. lang .. PS .. "LC_MESSAGES" .. PS .. fname .. " " .. line)
end
else
log.msg(log.screen, "ERROR: msgfmt executable not found. Please install or specifiy location in preferences.")
end
dt.preferences.register("executable_paths", "msgfmt", -- name
"file", -- type
'gen_i18n_mo: msgfmt location', -- label
'Install location of msgfmt. Requires restart to take effect.', -- tooltip
"msgfmt", -- default
dt.new_widget("file_chooser_button"){
title = "Select msgfmt[.exe] file",
value = "",
is_directory = false,
}
)

View File

@@ -10,7 +10,7 @@ local df = require "lib/dtutils.file"
local log = require "lib/dtutils.log"
local libname = nil
dt.configuration.check_version(...,{3,0,0})
du.check_min_api_version("3.0.0", "get_lib_manpages")
local keys = {"Name", "Synopsis", "Usage", "Description", "Return_Value", "Limitations",
"Example", "See_Also", "Reference", "License", "Copyright"}

View File

@@ -5,8 +5,9 @@
]]
local dt = require "darktable"
local du = require "lib/dtutils"
dt.configuration.check_version(...,{3,0,0})
du.check_min_api_version("3.0.0", "get_libdoc")
local keys = {"Name", "Synopsis", "Usage", "Description", "Return_Value", "Limitations",
"Example", "See_Also", "Reference", "License", "Copyright"}

774
tools/script_manager.lua Normal file
View File

@@ -0,0 +1,774 @@
--[[
This file is part of darktable,
copyright (c) 2018 Bill Ferguson <wpferguson@gmail.com>
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[
script_manager.lua - a tool for managing the darktable lua scripts
script_manager is designed to run as a standalone script so that it
may be used as a drop in luarc file in the user's $HOME/.config/darktable
($HOME/AppData/Local/darktable on windows) directory. It may also be
required from a luarc file.
On startup script_manager checks to see if there is an existing scripts directory.
If there is an existing lua scripts directory then it is read to see what scripts are present.
Scripts are sorted by "category" based on what subdirectory they are found in, thus with a lua
scripts directory that matched the current repository the categories would be contrib, examples,
offical, and tools. Each script has an Enable/Disable button to enable or disable the script.
A link is created to the user's Downloads directory on linux, unix and MacOS. Windows users must create the
link manually using mklink.exe. Additional "un-official" scripts may be downloaded
from other sources and placed in the users Downloads directory. These scripts all fall in a downloads category.
They also each have an Enable/Disable button.
]]
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
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("script_manager",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("script_manager", msgid)
end
collectgarbage("stop")
-- - - - - - - - - - - - - - - - - - - - - - - -
-- C O N S T A N T S
-- - - - - - - - - - - - - - - - - - - - - - - -
-- path separator
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
-- command separator
local CS = dt.configuration.running_os == "windows" and "&" or ";"
local LUA_DIR = dt.configuration.config_dir .. PS .. "lua"
local LUA_SCRIPT_REPO = "https://github.com/darktable-org/lua-scripts.git"
dt.print_log("LUA_DIR is " .. LUA_DIR)
-- - - - - - - - - - - - - - - - - - - - - - - -
-- N A M E S P A C E
-- - - - - - - - - - - - - - - - - - - - - - - -
local script_manager = {}
local sm = script_manager
-- - - - - - - - - - - - - - - - - - - - - - - -
-- F U N C T I O N S
-- - - - - - - - - - - - - - - - - - - - - - - -
local function prequire(script)
dt.print_log("Loading " .. script)
local status, lib = pcall(require, script)
if status then
dt.print_log("Loaded " .. script)
else
dt.print_error("Error loading " .. script)
dt.print_error(lib)
end
return status, lib
end
local function update_combobox_choices(combobox, choice_table, selected)
local items = #combobox
local choices = #choice_table
for i, name in ipairs(choice_table) do
combobox[i] = name
end
if choices < items then
for j = items, choices + 1, -1 do
combobox[j] = nil
end
end
combobox.value = selected
end
local function install_scripts()
local result = false
if df.check_if_file_exists(LUA_DIR) then
if df.check_if_file_exists(LUA_DIR .. ".orig") then
if dt.configuration.running_os == "windows" then
os.execute("rmdir /s " .. LUA_DIR .. ".orig")
else
os.execute("rm -rf " .. LUA_DIR .. ".orig")
end
end
os.rename(LUA_DIR, LUA_DIR .. ".orig")
end
local git = df.check_if_bin_exists("git")
if not git then
dt.print("ERROR: git not found. Install or specify the location of the git executable.")
return
end
local git_command = git .. " clone " .. sm.lua_repository .. " " .. LUA_DIR
dt.print_log("install git command is " .. git_command)
result = os.execute(git_command)
if not df.check_if_file_exists(LUA_DIR .. PS .. "downloads") then
os.execute("mkdir " .. LUA_DIR .. PS .. "downloads")
end
return result
end
local function update_scripts()
local result = false
local git = df.check_if_bin_exists("git")
if not git then
dt.print("ERROR: git not found. Install or specify the location of the git executable.")
return
end
local git_command = "cd " .. LUA_DIR .. " " .. CS .. " " .. git .. " pull"
dt.print_log("update git command is " .. git_command)
if dt.configuration.running_os == "windows" then
result = dtsys.windows_command(git_command)
else
result = os.execute(git_command)
end
if dt.preferences.read("script_manager", "use_lua_scripts_version", "bool") and
dt.configuration.running_os == "windows" then
use_lua_scripts_version()
end
return result
end
local function add_script_data(script_file)
-- the script file supplied is category/filename.filetype
-- the following pattern splits the string into category, path, name, fileename, and filetype
-- for example contrib/gimp.lua becomes
-- category - contrib
-- path -
-- name - gimp.lua
-- filename - gimp
-- filetype - lua
-- Thanks Tobias Jakobs for the awesome regulary expression
local pattern = "(.-)/(.-)(([^\\/]-)%.?([^%.\\/]*))$"
if dt.configuration.running_os == "windows" then
-- change the path separator from / to \ for windows
pattern = "(.-)\\(.-)(([^\\]-)%.?([^%.\\]*))$"
end
dt.print_log("processing " .. script_file)
-- add the script data
local category,path,name,filename,filetype = string.match(script_file, pattern)
dt.print_log("category is " .. category)
dt.print_log("name is " .. name)
if #sm.script_categories == 0 or not string.match(du.join(sm.script_categories, " "), category) then
sm.script_categories[#sm.script_categories + 1] = category
sm.script_names[category] = {}
end
if name then
dt.print_log("category is " .. category)
dt.print_log("name is " .. name)
if not string.match(du.join(sm.script_names[category], " "), name) then
sm.script_names[category][#sm.script_names[category] + 1] = name
sm.script_paths[category .. "/" .. name] = category .. "/" .. path .. name
if category == "downloads" then
sm.have_downloads = true
end
end
end
end
local function scan_scripts()
local find_cmd = "find -L " .. LUA_DIR .. " -name \\*.lua -print | sort"
if dt.configuration.running_os == "windows" then
find_cmd = "dir /b/s " .. LUA_DIR .. "\\*.lua | sort"
end
-- scan the scripts
local output = io.popen(find_cmd)
for line in output:lines() do
local l = string.gsub(line, LUA_DIR .. PS, "") -- strip the lua dir off
local script_file = l:sub(1,-5)
if not string.match(script_file, "script_manager") then -- let's not include ourself
if not string.match(script_file, "plugins") then -- skip plugins
if not string.match(script_file, "lib" .. PS) then -- let's not try and run libraries
if not string.match(script_file, "include_all") then -- skip include_all.lua
if not string.match(script_file, "yield") then -- special case, because everything needs this
add_script_data(script_file)
else
prequire(script_file) -- load yield.lua
end
end
end
end
end
end
-- work around because we can't dynamically add a new stack child. We create an empty child that will be
-- populated with downloads as they occur. If there are already downloads then this is just ignored
add_script_data("downloads" .. PS)
end
-- get the script documentation, with some assumptions
local function get_script_doc(script)
local description = nil
f = io.open(LUA_DIR .. PS .. script .. ".lua")
if f then
-- slurp the file
local content = f:read("*all")
f:close()
-- assume that the second block comment is the documentation
description = string.match(content, "%-%-%[%[.-%]%].-%-%-%[%[(.-)%]%]")
else
dt.print_error("Cant read from " .. script)
end
if description then
return description
else
return "No documentation available"
end
end
local function activate(script, scriptname)
dt.print_log("activating " .. scriptname)
local status, err = prequire(sm.script_paths[script])
if status then
dt.preferences.write("script_manager", script, "bool", true)
dt.print("Loaded " .. scriptname)
else
dt.print(scriptname .. " failed to load")
dt.print_error("Error loading " .. scriptname)
dt.print_error("Error message: " .. err)
end
return status
end
local function deactivate(script, scriptname)
-- presently the lua api doesn't support unloading gui elements therefore
-- we just mark then inactive for the next time darktable starts
-- deactivate it....
dt.preferences.write("script_manager", script, "bool", false)
dt.print_log("setting " .. scriptname .. " to not start")
dt.print(scriptname .. " will not be active when darktable is restarted")
end
local function create_enable_disable_button(btext, sname, req)
return dt.new_widget("button")
{
label = btext .. sname,
tooltip = get_script_doc(req),
clicked_callback = function (self)
-- split the label into action and target
local action, target = string.match(self.label, "(.+) (.+)")
-- load the script if it's not loaded
dt.print_log("Looking for target " .. target)
local scat = sm.category_selector.value
-- check, and fix, the filename for a - since it is a lua magic character in string match
dt.print_log("checking category " .. scat .. " for target " .. target)
local starget = du.join({scat, target}, "/")
dt.print_log("starget to activate is " .. starget)
dt.print_log("target to activate is " .. target)
if action == "Enable" then
local status = activate(starget, target)
if status then
self.label = "Disable " .. target
end
else
deactivate(starget, target)
self.label = "Enable " .. target
end
end
}
end
local function load_script_stack()
-- load the scripts
table.sort(sm.script_categories)
for _,cat in ipairs(sm.script_categories) do
local tmp = {}
table.sort(sm.script_names[cat])
if not sm.script_widgets[cat] then
for _,sname in ipairs(sm.script_names[cat]) do
local req = du.join({cat, sname}, "/")
local btext = "Enable "
if dt.preferences.read("script_manager", req, "bool") then
local status, err = prequire(sm.script_paths[req])
if status then
btext = "Disable "
else
dt.print_error("Error loading " .. sname)
dt.print_error("Error message: " .. err)
end
else
dt.preferences.write("script_manager", req, "bool", false)
end
tmp[#tmp + 1] = create_enable_disable_button(btext, sname, req)
end
sm.script_widgets[cat] = dt.new_widget("box")
{
orientation = "vertical",
table.unpack(tmp),
}
elseif #sm.script_widgets[cat] ~= #sm.script_names[cat] then
for index,sname in ipairs(sm.script_names[cat]) do
local req = du.join({cat, sname}, "/")
dt.print_error("script is " .. sname .. " and index is " .. index)
if sm.script_widgets[cat][index] then
sm.script_widgets[cat][index] = nil
end
sm.script_widgets[cat][index] = create_enable_disable_button("Enable ", sname, req)
end
end
end
if not sm.script_stack then
sm.script_stack = dt.new_widget("stack"){}
for i,cat in ipairs(sm.script_categories) do
sm.script_stack[i] = sm.script_widgets[cat]
end
sm.script_stack.active = 1
end
end
local function update_stack_choices(combobox, choice_table)
sm.have_downloads = true
local items = #combobox
local choices = #choice_table
if #sm.script_widgets["downloads"] == 0 then
choices = choices - 1
sm.have_downloads = false
end
cnt = 1
for i, name in ipairs(choice_table) do
if (name == "downloads" and sm.have_downloads) or name ~= "downloads" then
combobox[cnt] = name
cnt = cnt + 1
end
end
if choices < items then
for j = items, choices + 1, -1 do
combobox[j] = nil
end
end
combobox.value = 1
end
local function build_scripts_block()
-- build the whole script block
scan_scripts()
-- set up the stack for the choices
load_script_stack()
if not sm.category_selector then
-- set up the combobox for the categories
sm.category_selector = dt.new_widget("combobox"){
label = "Category",
tooltip = "Select the script category",
value = 1, "placeholder",
changed_callback = function(self)
local cnt = 1
for i,cat in ipairs(sm.script_categories) do
if cat == self.value then
sm.script_stack.active = i
end
end
end
}
end
update_stack_choices(sm.category_selector, sm.script_categories)
if not sm.scripts then
sm.scripts = dt.new_widget("box"){
orientation = "vertical",
dt.new_widget("label"){ label = "Scripts" },
sm.category_selector,
sm.script_stack,
}
end
end
local function insert_scripts_block()
table.insert(sm.main_menu_choices, "Enable/Disable Scripts")
update_combobox_choices(sm.main_menu, sm.main_menu_choices, 1)
sm.main_stack[#sm.main_stack + 1] = sm.scripts
end
local function use_lua_scripts_version()
if dt.configuration.running_os == "windows" then
-- copy tools\script_manger.lua to luarc
df.file_copy(LUA_DIR .. PS .. "tools\\script_manager.lua", dt.configuration.config_dir .. PS .. "luarc")
else
-- create a symbolic link from luarc to tools/script_manager.lua
if df.check_if_file_exists(dt.configuration.config_dir .. "/luarc") then
os.remove(dt.configuration.config_dir .. "/luarc")
end
os.execute("ln -s " .. LUA_DIR .. "/tools/script_manager.lua " .. dt.configuration.config_dir .. "/luarc")
end
end
local function link_downloads_directory()
if not df.check_if_file_exists("$HOME/Downloads") then
os.execute("mkdir $HOME/Downloads")
end
if df.check_if_file_exists(LUA_DIR .. "/downloads") then
os.remove(LUA_DIR .. "/downloads")
end
os.execute("ln -s " .. "$HOME/Downloads " .. LUA_DIR .. "/downloads")
end
-- - - - - - - - - - - - - - - - - - - - - - - -
-- M A I N P R O G R A M
-- - - - - - - - - - - - - - - - - - - - - - - -
-- api check
du.check_min_api_version("5.0.0")
-- set up tables to contain all the widgets and choices
sm.script_widgets = {}
sm.script_categories = {}
sm.script_names = {}
sm.script_paths = {}
sm.main_menu_choices = {}
sm.main_stack_items = {}
-- see if we've run this before
sm.initialized = dt.preferences.read("script_manager", "initialized", "bool")
if not sm.initialized then
-- write out preferences
dt.preferences.write("script_manager", "lua_repository", "string", LUA_SCRIPT_REPO)
dt.preferences.write("script_manager", "use_distro_version", "bool", false)
dt.preferences.write("script_manager", "link_downloads_directory", "bool", false)
dt.preferences.write("script_manager", "initialized", "bool", true)
end
sm.have_scripts = df.check_if_file_exists(LUA_DIR)
sm.git_managed = df.check_if_file_exists(LUA_DIR .. PS .. ".git")
if sm.have_scripts then
dt.print_log("found lua scripts directory")
else
dt.print_log("lua scripts directory not found")
end
if sm.git_managed then
dt.print_log("scripts managed by git")
else
dt.print_log("scripts not managed")
end
local git = df.check_if_bin_exists("git")
if git then
dt.print_log("git found at " .. git)
sm.need_git = false
else
dt.print_log("git not found")
sm.need_git = true
end
sm.repository = dt.new_widget("entry")
{
text = dt.preferences.read("script_manager", "lua_repository", "string"),
editable = true,
}
sm.need_install = false
local install_update_text = _("install")
if sm.have_scripts and sm.git_managed then
install_update_text = _("update")
else
sm.need_install = true
end
sm.install_update_button = dt.new_widget("button"){
label = install_update_text .. _(" scripts"),
clicked_callback = function(self)
sm.lua_repository = sm.repository.text
dt.preferences.write("script_manager", "lua_repository", "string", sm.repository.text)
if sm.need_install then
local result = install_scripts()
if result then
build_scripts_block()
insert_scripts_block()
sm.have_scripts = true
sm.git_managed = true
sm.need_install = false
self.label = _("update scripts")
dt.print(_("installed scripts from " .. sm.repository.text))
else
dt.print(_("Error installing scripts from " .. sm.repository.text))
end
else
local result = update_scripts()
if result then
--build_scripts_block()
dt.print(_("updated scripts from " .. sm.repository.text))
else
dt.print(_("Error updating scripts from " .. sm.repository.text))
end
end
end
}
if not sm.need_install then
sm.reinstall_button = dt.new_widget("button"){
label = "reinstall scripts",
clicked_callback = function(self)
sm.lua_repository = sm.repository.text
dt.preferences.write("script_manager", "lua_repository", "string", sm.repository.text)
local result = install_scripts()
if result then
build_scripts_block()
insert_scripts_block()
sm.have_scripts = true
sm.git_managed = true
sm.need_install = false
sm.install_update_button.label = "update scripts"
dt.print(_("reinstalled scripts from " .. sm.repository.text))
dt.print_log(_("scripts reinstalled"))
else
dt.print(_("ERROR: script reinstallation failed"))
dt.print_error(_("script reinstall failed"))
end
end
}
sm.install_update_widgets = {
sm.install_update_button,
sm.reinstall_button,
}
else
sm.install_update_widgets = {
sm.install_update_button,
}
end
sm.install_update_box = dt.new_widget("box"){
orientation = "vertical",
dt.new_widget("label"){ label = "Install/Update scripts" },
table.unpack(sm.install_update_widgets),
}
table.insert(sm.main_menu_choices, "Install/Update Scripts")
table.insert(sm.main_stack_items, sm.install_update_box)
-- configuration items
sm.repository_update = dt.new_widget("button"){
label = "update",
clicked_callback = function()
dt.preferences.write("script_manager", "lua_repository", "string", sm.repository.text)
sm.install_update_button.label = "install"
sm.install_update_button.sensitive = true
sm.reinstall_button.sensitive = false
sm.need_install = true
end
}
sm.repository_reset = dt.new_widget("button"){
label = "reset",
clicked_callback = function()
sm.repository.text = LUA_SCRIPT_REPO
dt.preferences.write("script_manager", "lua_repository", "string", LUA_SCRIPT_REPO)
sm.install_update_button.label = "install"
sm.install_update_button.sensitive = true
sm.reinstall_button.sensitive = false
sm.need_install = true
end
}
sm.update_reset = dt.new_widget("box"){
orientation = "horizontal",
sm.repository_update,
sm.repository_reset,
}
-- replace standalone version of script_manager with distributed version, i.e. tools/script_manager.lua
-- link on linux/MacOS, copy on windows
-- this option won't be active until the repository version of this script is accepted
sm.use_lua_scripts_version = dt.new_widget("check_button"){
label = "Use lua scripts distributed version",
tooltip = "Use the standalone version (false) or the distributed version (true)",
value = dt.preferences.read("script_manager", "use_distro_version", "bool"),
clicked_callback = function(self)
if dt.preferences.read("script_manager", "use_distro_version", "bool") == self.value then
-- do nothing
else
sm.apply_configuration.sensitive = true
end
end
}
-- link downloads to $HOME/downloads on linux and MacOS
sm.link_downloads_directory = dt.new_widget("check_button"){
label = "Link lua/downloads to $HOME/Downloads",
tooltip = "Linking the directories enables dropping a script in $HOME/downloads\nand having it recognized the next time darktable starts",
value = dt.preferences.read("script_manager", "link_downloads_directory", "bool"),
clicked_callback = function(self)
if dt.preferences.read("script_manager", "link_downloads_directory", "bool") == self.value then
-- do nothing
else
sm.apply_configuration.sensitive = true
end
end
}
sm.apply_configuration = dt.new_widget("button"){
label = "Apply",
sensitive = false,
clicked_callback = function(self)
dt.preferences.write("script_manager", "use_distro_version", "bool", sm.use_lua_scripts_version.value)
if sm.use_lua_scripts_version.value then
use_lua_scripts_version()
end
if dt.configuration.running_os ~= "windows" then
dt.preferences.write("script_manager", "link_downloads_directory", "bool", sm.link_downloads_directory.value)
if sm.link_downloads_directory.value then
link_downloads_directory()
end
end
end
}
-- get git location on windows
sm.git_location = df.executable_path_widget({"git"})
sm.configuration_widgets = {
sm.repository,
sm.update_reset,
dt.new_widget("separator"){},
dt.new_widget("separator"){},
}
if dt.configuration.running_os == "windows" or sm.need_git then
sm.configuration_widgets[#sm.configuration_widgets + 1] = sm.git_location
end
if dt.configuration.running_os ~= "windows" then
sm.configuration_widgets[#sm.configuration_widgets + 1] = sm.use_lua_scripts_version
sm.configuration_widgets[#sm.configuration_widgets + 1] = sm.link_downloads_directory
end
sm.configuration_widgets[#sm.configuration_widgets + 1] = sm.apply_configuration
sm.config_box = dt.new_widget("box"){
orientation = "vertical",
dt.new_widget("label") { label = "Configuration" },
table.unpack(sm.configuration_widgets),
}
table.insert(sm.main_menu_choices, "Configure")
table.insert(sm.main_stack_items, sm.config_box)
-- set up the outside stack for config, install/update, and download
-- make a stack for the choices
sm.main_stack = dt.new_widget("stack"){
table.unpack(sm.main_stack_items),
}
-- make a combobox for the selector
sm.main_menu = dt.new_widget("combobox"){
label = "Action",
tooltip = "Select the action you want to perform",
value = 1, "No actions available",
changed_callback = function(self)
for pos,str in ipairs(sm.main_menu_choices) do
if self.value == str then
sm.main_stack.active = pos
dt.preferences.write("script_manager", "sm_main_menu_value", "integer", pos)
end
end
end
}
if #sm.main_menu_choices > 0 then
update_combobox_choices(sm.main_menu, sm.main_menu_choices, 1)
end
sm.main_box = dt.new_widget("box"){
orientation = "vertical",
sm.main_menu,
sm.main_stack,
}
-- - - - - - - - - - - - - - - - - - - - - - - -
-- D A R K T A B L E I N T E G R A T I O N
-- - - - - - - - - - - - - - - - - - - - - - - -
-- register the module
dt.register_lib(
"script_manager", -- Module name
"script manager", -- Visible name
true, -- expandable
false, -- resetable
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 0}}, -- containers
dt.new_widget("box") -- widget
{
orientation = "vertical",
sm.main_box,
},
nil,-- view_enter
nil -- view_leave
)
-- set up the scripts block if we have them otherwise we'll wait until we download them
if sm.have_scripts then
-- scan for scripts and populate the categories
build_scripts_block()
-- add the widgets to the lib
insert_scripts_block()
sm.main_menu.selected = 3
end
collectgarbage("restart")