Compare commits
136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96ba075e86 | ||
|
|
e86a3a3236 | ||
|
|
b41b2a1566 | ||
|
|
b893383353 | ||
|
|
ddcf3ed3f4 | ||
|
|
da05748393 | ||
|
|
7790c9700b | ||
|
|
1d5192a39c | ||
|
|
04525e1258 | ||
|
|
4c536b41bc | ||
|
|
55ced3ecc7 | ||
|
|
4a81b560f9 | ||
|
|
6a7cd31ab4 | ||
|
|
2515db31d8 | ||
|
|
54a66f9ab2 | ||
|
|
0856858e73 | ||
|
|
d579b257a8 | ||
|
|
8a8f6338c5 | ||
|
|
71207e8f57 | ||
|
|
749f5b8e8f | ||
|
|
88c1be09c2 | ||
|
|
c1b59256be | ||
|
|
a3f57665a9 | ||
|
|
b8d29ca831 | ||
|
|
90880ff4d1 | ||
|
|
0bb8b72183 | ||
|
|
91ab85c825 | ||
|
|
8c75b9316d | ||
|
|
9870ddbe86 | ||
|
|
c7eb101920 | ||
|
|
2b171aef7c | ||
|
|
ec0fe0b047 | ||
|
|
bbfa81f051 | ||
|
|
bd5bef279c | ||
|
|
afa95d5d34 | ||
|
|
fb331710e1 | ||
|
|
224dce1b76 | ||
|
|
a582f0229f | ||
|
|
ebd33c4c16 | ||
|
|
3f5cf06bd9 | ||
|
|
1ec9cfb772 | ||
|
|
050175802f | ||
|
|
19a1be1f74 | ||
|
|
093593dcef | ||
|
|
51f28a386b | ||
|
|
4065bd5e11 | ||
|
|
3e1746fc6e | ||
|
|
3b6dd856aa | ||
|
|
635ea3f231 | ||
|
|
48d1c62fed | ||
|
|
e97f2f3fc6 | ||
|
|
19b016a3dd | ||
|
|
576e160372 | ||
|
|
e586e0479d | ||
|
|
0b5c866df6 | ||
|
|
9828fb3934 | ||
|
|
430f443916 | ||
|
|
d32e0eb5ab | ||
|
|
f72750576f | ||
|
|
e96b78b175 | ||
|
|
4708110804 | ||
|
|
2725b82543 | ||
|
|
1eafae6c6f | ||
|
|
11e25a21fa | ||
|
|
51eda4e755 | ||
|
|
847c39749d | ||
|
|
cb0975fa69 | ||
|
|
5b977733b3 | ||
|
|
c3d13a9890 | ||
|
|
bc4c04f795 | ||
|
|
32d5240e5d | ||
|
|
64f423f3d1 | ||
|
|
18d0e974e4 | ||
|
|
c259f2dc72 | ||
|
|
5223fdf4ae | ||
|
|
a4132bd9bc | ||
|
|
bf8d147ca5 | ||
|
|
5116d645db | ||
|
|
a2850cac6b | ||
|
|
ffed4efdec | ||
|
|
cfa71927de | ||
|
|
5d7a60206e | ||
|
|
3556fef5ed | ||
|
|
252965a5d9 | ||
|
|
a7a25c0798 | ||
|
|
944087649f | ||
|
|
69c9db6dda | ||
|
|
f5e86a8d25 | ||
|
|
6a89357b54 | ||
|
|
64c8f8fc3e | ||
|
|
54ff56b021 | ||
|
|
3d3ad89fba | ||
|
|
7ee140c8c8 | ||
|
|
8afd6ccee4 | ||
|
|
e427294ec1 | ||
|
|
18283ccbc6 | ||
|
|
b7857c8462 | ||
|
|
2d231806d2 | ||
|
|
36b58333ca | ||
|
|
fc77af598d | ||
|
|
602295ae89 | ||
|
|
1c295a88ac | ||
|
|
232535faf9 | ||
|
|
10b5dee40f | ||
|
|
be48459092 | ||
|
|
cb6199c1a8 | ||
|
|
c9eb087ade | ||
|
|
87e223a854 | ||
|
|
7c7118dfad | ||
|
|
fdcd6c6dfe | ||
|
|
205cbcf05d | ||
|
|
c9ccbb2750 | ||
|
|
eacfc4b623 | ||
|
|
d3dafd4ce1 | ||
|
|
8004b96c41 | ||
|
|
7cc58d9fac | ||
|
|
f82ae6c091 | ||
|
|
fcad7a5733 | ||
|
|
7ffdf1631c | ||
|
|
a5ad5e67f3 | ||
|
|
4b3b1f7786 | ||
|
|
844e89a652 | ||
|
|
d88af9ffd9 | ||
|
|
7a3a9ba559 | ||
|
|
5d3910da49 | ||
|
|
f740d34f1f | ||
|
|
58ca70c511 | ||
|
|
ee59540648 | ||
|
|
85c242ff0e | ||
|
|
7bc4846c0a | ||
|
|
5fd3a56a31 | ||
|
|
2cb3397d75 | ||
|
|
2a2fbd18cd | ||
|
|
11607e7e7d | ||
|
|
8e6552ee9c | ||
|
|
024e016df2 |
35
README.md
35
README.md
@@ -32,28 +32,36 @@ These scripts are contributed by users. They are meant to have an "owner", i.e.
|
||||
|
||||
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
|
||||
video_mencoder|No|L|Export video from darktable
|
||||
transfer_hierarchy|Yes|LMW|Image move/copy preserving directory hierarchy
|
||||
video_ffmpeg|No|LMW|Export video from darktable
|
||||
|
||||
### Example Scripts
|
||||
|
||||
@@ -62,10 +70,13 @@ These scripts provide examples of how to use specific portions of the API. They
|
||||
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
|
||||
@@ -82,6 +93,22 @@ get_lib_manpages|No|LM|Retrieve the library documentation and output it in man p
|
||||
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
|
||||
@@ -116,6 +143,8 @@ 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"`.
|
||||
@@ -127,8 +156,8 @@ or `echo 'require "contrib/hugin"' >> ~/.config/darktable/luarc` to add an entry
|
||||
|
||||
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.
|
||||
`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
|
||||
|
||||
|
||||
424
contrib/HDRMerge.lua
Normal file
424
contrib/HDRMerge.lua
Normal 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
|
||||
}
|
||||
)
|
||||
@@ -2,6 +2,8 @@
|
||||
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
|
||||
@@ -18,18 +20,23 @@ OpenInExplorer plugin for darktable
|
||||
]]
|
||||
|
||||
--[[About this plugin
|
||||
This plugin adds the module "OpenInExplorer" to darktable's lighttable view
|
||||
This plugin adds the module "OpenInExplorer" to darktable's lighttable view.
|
||||
|
||||
----REQUIRED SOFTWARE----
|
||||
Microsoft Windows or Linux with installed Nautilus
|
||||
Apple macOS, Microsoft Windows or Linux
|
||||
|
||||
----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.
|
||||
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 explorer and press "Go to Folder".
|
||||
A file explorer window will be opened for each selected file at the file's location; the file will be highlighted.
|
||||
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----
|
||||
]]
|
||||
@@ -38,76 +45,67 @@ 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")
|
||||
|
||||
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
|
||||
gettext.bindtextdomain("OpenInExplorer",dt.configuration.config_dir.."/lua/locale/")
|
||||
|
||||
--Detect OS and modify accordingly--
|
||||
local proper_install = false
|
||||
if dt.configuration.running_os ~= "macos" then
|
||||
proper_install = true
|
||||
else
|
||||
dt.print_error('OpenInExplorer plug-in only supports Windows and Linux at this time')
|
||||
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'
|
||||
|
||||
-- FUNCTIONS --
|
||||
|
||||
local function open_in_explorer() --Open in Explorer
|
||||
--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 = ""
|
||||
local curr_image, run_cmd, file_uris = '', '', ''
|
||||
if #images == 0 then
|
||||
dt.print('please select an image')
|
||||
dt.print(_('Please select an image'))
|
||||
elseif #images <= 15 then
|
||||
for _,image in pairs(images) do
|
||||
for _,image in pairs(images) do
|
||||
curr_image = image.path..PS..image.filename
|
||||
local run_cmd = "explorer.exe /select, "..curr_image
|
||||
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)
|
||||
resp = dsys.external_command(run_cmd)
|
||||
dsys.external_command(run_cmd)
|
||||
end
|
||||
else
|
||||
dt.print('please select fewer images (max 15)')
|
||||
dt.print(_('Please select fewer images (max 15)'))
|
||||
end
|
||||
end
|
||||
|
||||
local function open_in_nautilus() --Open in Nautilus
|
||||
local images = dt.gui.selection()
|
||||
local curr_image = ""
|
||||
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
|
||||
local run_cmd = [[busctl --user call org.freedesktop.FileManager1 /org/freedesktop/FileManager1 org.freedesktop.FileManager1 ShowItems ass 1 ]] .. df.sanitize_filename("file://"..curr_image) .. [[ ""]]
|
||||
dt.print_log("OpenInExplorer run_cmd = "..run_cmd)
|
||||
resp = dsys.external_command(run_cmd)
|
||||
end
|
||||
else
|
||||
dt.print('please select fewer images (max 15)')
|
||||
end
|
||||
end
|
||||
|
||||
local function open_in_filemanager() --Open
|
||||
--Inits--
|
||||
if not proper_install then
|
||||
return
|
||||
end
|
||||
|
||||
if (dt.configuration.running_os == "windows") then
|
||||
open_in_explorer()
|
||||
elseif (dt.configuration.running_os == "linux") then
|
||||
open_in_nautilus()
|
||||
end
|
||||
end
|
||||
|
||||
-- GUI --
|
||||
if proper_install then
|
||||
dt.gui.libs.image.register_action(
|
||||
"show in file explorer",
|
||||
function() open_in_filemanager() end,
|
||||
"Opens File Explorer at the selected image's location"
|
||||
_("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
212
contrib/RL_out_sharp.lua
Normal 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;
|
||||
File diff suppressed because it is too large
Load Diff
438
contrib/ext_editor.lua
Normal file
438
contrib/ext_editor.lua
Normal 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;
|
||||
491
contrib/face_recognition.lua
Normal file
491
contrib/face_recognition.lua
Normal 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
|
||||
358
contrib/transfer_hierarchy.lua
Executable file
358
contrib/transfer_hierarchy.lua
Executable 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)
|
||||
@@ -1,124 +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 du = require "lib/dtutils"
|
||||
local df = require "lib/dtutils.file"
|
||||
require "official/yield"
|
||||
local gettext = dt.gettext
|
||||
|
||||
du.check_min_api_version("2.0.1", "video_mencoder")
|
||||
|
||||
-- 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)
|
||||
dt.print("video_mencoder is deprecated. Please use video_ffmpeg instead. Please see the log for more details.")
|
||||
dt.print_log("The mencoder executable is no longer distributed as part of most distributions,")
|
||||
dt.print_log("therefore video_mencoder is deprecated. video_ffmpeg should be used in it's place. video_mencoder")
|
||||
dt.print_log("will be removed when darktable 2.8 is released.")
|
||||
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)
|
||||
98
examples/darkroom_demo.lua
Normal file
98
examples/darkroom_demo.lua
Normal 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)
|
||||
|
||||
198
examples/lighttable_demo.lua
Normal file
198
examples/lighttable_demo.lua
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -35,35 +35,54 @@ local du = require "lib/dtutils"
|
||||
|
||||
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
|
||||
@@ -71,69 +90,35 @@ 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")
|
||||
-- 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
|
||||
{
|
||||
label = "MySectionLabel"
|
||||
}
|
||||
|
||||
dt.register_lib(
|
||||
"exampleModule", -- Module name
|
||||
"exampleModule", -- name
|
||||
true, -- expandable
|
||||
false, -- resetable
|
||||
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers
|
||||
dt.new_widget("box") -- widget
|
||||
orientation = "vertical",
|
||||
dt.new_widget("button")
|
||||
{
|
||||
orientation = "vertical",
|
||||
dt.new_widget("button")
|
||||
{
|
||||
label = "MyButton",
|
||||
clicked_callback = function (_)
|
||||
dt.print("Button clicked")
|
||||
end
|
||||
},
|
||||
check_button,
|
||||
combobox,
|
||||
entry,
|
||||
file_chooser_button,
|
||||
label,
|
||||
separator,
|
||||
slider,
|
||||
section_label
|
||||
label = _("MyButton"),
|
||||
clicked_callback = function (_)
|
||||
dt.print(_("Button clicked"))
|
||||
end
|
||||
},
|
||||
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")
|
||||
end
|
||||
},
|
||||
check_button,
|
||||
combobox,
|
||||
entry,
|
||||
file_chooser_button,
|
||||
label,
|
||||
separator,
|
||||
slider
|
||||
},
|
||||
nil,-- view_enter
|
||||
nil -- view_leave
|
||||
)
|
||||
end
|
||||
check_button,
|
||||
combobox,
|
||||
entry,
|
||||
file_chooser_button,
|
||||
label,
|
||||
separator,
|
||||
slider
|
||||
},
|
||||
nil,-- view_enter
|
||||
nil -- view_leave
|
||||
)
|
||||
|
||||
-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua
|
||||
-- kate: hl Lua;
|
||||
|
||||
136
examples/panels_demo.lua
Normal file
136
examples/panels_demo.lua
Normal 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
|
||||
@@ -1,61 +0,0 @@
|
||||
--[[
|
||||
This file is part of darktable,
|
||||
copyright (c) 2014 Jérémy Rosen
|
||||
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/>.
|
||||
]]
|
||||
--[[
|
||||
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"
|
||||
|
||||
-- must be loaded for scripts using darktable.control_execute to work
|
||||
require "official/yield"
|
||||
|
||||
dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0})
|
||||
|
||||
-- find all scripts, but skip the lib and tools directories
|
||||
local output = io.popen("cd "..dt.configuration.config_dir.."/lua ;find . -name lib -prune -o -name tools -prune -o -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 and not string.match(req_name, "yield") 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
|
||||
@@ -35,6 +35,315 @@ local function _(msgid)
|
||||
return gettext.dgettext("dtutils.file", msgid)
|
||||
end
|
||||
|
||||
--[[
|
||||
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.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 = [[]],
|
||||
Reference = [[]],
|
||||
License = [[]],
|
||||
Copyright = [[]],
|
||||
}
|
||||
|
||||
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
|
||||
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]],
|
||||
@@ -44,14 +353,25 @@ dtutils_file.libdoc.functions["check_if_bin_exists"] = {
|
||||
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. The presence of the file is verified, then
|
||||
quoted and returned. If no preference is specified and the operating system is
|
||||
linux then the which command is used to check for a binary in the path. If found
|
||||
that path is returned. If no binary is found, false is returned.]],
|
||||
Return_Value = [[result - string - the path of the binary, false if not found]],
|
||||
Limitations = [[]],
|
||||
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 = [[]],
|
||||
See_Also = [[executable_manager]],
|
||||
Reference = [[]],
|
||||
License = [[]],
|
||||
Copyright = [[]],
|
||||
@@ -59,28 +379,15 @@ dtutils_file.libdoc.functions["check_if_bin_exists"] = {
|
||||
|
||||
function dtutils_file.check_if_bin_exists(bin)
|
||||
local result = false
|
||||
local path = nil
|
||||
|
||||
if string.match(bin, "/") or string.match(bin, "\\") then
|
||||
path = bin
|
||||
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
|
||||
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$")) and dt.configuration.running_os ~= "windows" then
|
||||
result = "wine " .. "\"" .. path .. "\""
|
||||
else
|
||||
result = "\"" .. path .. "\""
|
||||
end
|
||||
end
|
||||
elseif dt.configuration.running_os == "linux" then
|
||||
local p = io.popen("which " .. bin)
|
||||
local output = p:read("*a")
|
||||
p:close()
|
||||
if string.len(output) > 0 then
|
||||
result = output:sub(1,-2)
|
||||
result = _check_path_for_bin(bin)
|
||||
|
||||
if not result then
|
||||
result = _search_for_bin(bin)
|
||||
end
|
||||
end
|
||||
return result
|
||||
@@ -107,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
|
||||
|
||||
@@ -222,15 +534,21 @@ dtutils_file.libdoc.functions["check_if_file_exists"] = {
|
||||
}
|
||||
|
||||
function dtutils_file.check_if_file_exists(filepath)
|
||||
local result
|
||||
local result = false
|
||||
if (dt.configuration.running_os == 'windows') then
|
||||
filepath = string.gsub(filepath, '[\\/]+', '\\')
|
||||
result = os.execute('if exist "'..filepath..'" (cmd /c exit 0) else (cmd /c exit 1)')
|
||||
if not result then
|
||||
result = false
|
||||
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 ' .. "\"" .. filepath .. "\"")
|
||||
result = os.execute('test -e ' .. dtutils_file.sanitize_filename(filepath))
|
||||
if not result then
|
||||
result = false
|
||||
end
|
||||
@@ -266,7 +584,11 @@ dtutils_file.libdoc.functions["chop_filetype"] = {
|
||||
|
||||
function dtutils_file.chop_filetype(path)
|
||||
local length = dtutils_file.get_filetype(path):len() + 2
|
||||
return string.sub(path, 1, -length)
|
||||
if length > 2 then
|
||||
return string.sub(path, 1, -length)
|
||||
else
|
||||
return path
|
||||
end
|
||||
end
|
||||
|
||||
dtutils_file.libdoc.functions["file_copy"] = {
|
||||
@@ -373,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 = [[]],
|
||||
@@ -396,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-)$", "")
|
||||
@@ -433,8 +758,9 @@ 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(dtutils_file.get_basename(filepath), "_(d-)$") == "99" then
|
||||
break
|
||||
local increment = string.match(dtutils_file.get_basename(filepath), "_(%d-)$")
|
||||
if increment == "99" then
|
||||
break
|
||||
end
|
||||
end
|
||||
return filepath
|
||||
@@ -525,8 +851,9 @@ function dtutils_file.executable_path_widget(executables)
|
||||
if dtutils_file.check_if_bin_exists(self.value) then
|
||||
dtutils_file.set_executable_path_preference(executable, self.value)
|
||||
end
|
||||
end}
|
||||
)
|
||||
end
|
||||
}
|
||||
)
|
||||
end
|
||||
local box = dt.new_widget("box"){
|
||||
orientation = "vertical",
|
||||
@@ -576,6 +903,7 @@ dtutils_file.libdoc.functions["mkdir"] = {
|
||||
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"
|
||||
@@ -607,6 +935,53 @@ function dtutils_file.rmdir(path)
|
||||
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
|
||||
|
||||
|
||||
@@ -167,13 +167,66 @@ 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]],
|
||||
@@ -192,16 +245,29 @@ dtutils_string.libdoc.functions["sanitize"] = {
|
||||
Copyright = [[]],
|
||||
}
|
||||
|
||||
function dtutils_string.sanitize(str)
|
||||
local result = ""
|
||||
|
||||
if dt.configuration.running_os == "windows" then
|
||||
result = '"' .. str .. '"'
|
||||
local function _sanitize_posix(str)
|
||||
if _is_not_sanitized_posix(str) then
|
||||
return "'" .. string.gsub(str, "'", "'\\''") .. "'"
|
||||
else
|
||||
result = "'" .. str .. "'"
|
||||
return str
|
||||
end
|
||||
|
||||
return result
|
||||
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
|
||||
|
||||
39
locale/de_DE/LC_MESSAGES/OpenInExplorer.po
Normal file
39
locale/de_DE/LC_MESSAGES/OpenInExplorer.po
Normal 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"
|
||||
Binary file not shown.
Binary file not shown.
122
locale/it_IT/LC_MESSAGES/ext_editor.po
Normal file
122
locale/it_IT/LC_MESSAGES/ext_editor.po
Normal 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 "
|
||||
@@ -54,191 +54,210 @@ end
|
||||
-- is enfuse installed?
|
||||
local enfuse_installed = df.check_if_bin_exists("enfuse")
|
||||
|
||||
-- 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
|
||||
if enfuse_installed then
|
||||
dt.print_log("found enfuse executable at " .. enfuse_installed)
|
||||
|
||||
-- check the version so that we can use the correct arguments
|
||||
-- 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
|
||||
|
||||
local version = nil
|
||||
-- check the version so that we can use the correct arguments
|
||||
|
||||
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)
|
||||
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 = 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",
|
||||
hard_min = 0,
|
||||
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")
|
||||
{
|
||||
label = "depth",
|
||||
tooltip = "the number of bits per channel of the output image",
|
||||
value = dt.preferences.read("enfuse", "depth", "integer"),
|
||||
changed_callback = function(w) dt.preferences.write("enfuse", "depth", "integer", w.selected) end,
|
||||
"8", "16", "32"
|
||||
}
|
||||
|
||||
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), ",", ".")
|
||||
-- TODO: make filename unique
|
||||
local output_image = target_dir.. PS .. "enfuse.tif"
|
||||
local exposure_option = " --exposure-optimum "
|
||||
if version < "4.2" then
|
||||
exposure_option = " --exposure-mu "
|
||||
end
|
||||
local command = enfuse_installed.." --depth "..depth.value..exposure_option..ugly_decimal_point_hack
|
||||
.." -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))
|
||||
-- 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
|
||||
}
|
||||
|
||||
local lib_widgets = {}
|
||||
-- set up some widgets, initialized from config
|
||||
|
||||
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, enfuse_button)
|
||||
local exposure_mu = nil
|
||||
|
||||
|
||||
-- ... and tell dt about it all
|
||||
dt.register_lib(
|
||||
"enfuse", -- plugin name
|
||||
"enfuse", -- name
|
||||
true, -- expandable
|
||||
false, -- resetable
|
||||
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers
|
||||
dt.new_widget("box") -- widget
|
||||
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",
|
||||
hard_min = 0,
|
||||
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")
|
||||
{
|
||||
orientation = "vertical",
|
||||
sensitive = enfuse_installed,
|
||||
table.unpack(lib_widgets)
|
||||
},
|
||||
nil,-- view_enter
|
||||
nil -- view_leave
|
||||
)
|
||||
label = "depth",
|
||||
tooltip = "the number of bits per channel of the output image",
|
||||
value = dt.preferences.read("enfuse", "depth", "integer"),
|
||||
changed_callback = function(w) dt.preferences.write("enfuse", "depth", "integer", w.selected) end,
|
||||
"8", "16", "32"
|
||||
}
|
||||
|
||||
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
|
||||
"enfuse", -- name
|
||||
true, -- expandable
|
||||
false, -- resetable
|
||||
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers
|
||||
dt.new_widget("box") -- widget
|
||||
{
|
||||
orientation = "vertical",
|
||||
sensitive = enfuse_installed,
|
||||
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;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/home/bill/src/plugins/lua-scripts/tools/plugin_manager.lua
|
||||
@@ -747,7 +747,7 @@ dt.register_lib(
|
||||
"script manager", -- Visible name
|
||||
true, -- expandable
|
||||
false, -- resetable
|
||||
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_BOTTOM", 100}}, -- containers
|
||||
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 0}}, -- containers
|
||||
dt.new_widget("box") -- widget
|
||||
{
|
||||
orientation = "vertical",
|
||||
|
||||
Reference in New Issue
Block a user