Hi Kneops,
sorry, still can't halp you.
I've spent some time looking at darktable's Lua API to figure out
whether there would be an easy way to solve your problem, but there is
not.
I came up with a *very experimental* Lua script (attached) that can
call `exiv2` to modify metadata in XMP files, but it comes wit a *lot*
of caveats:
* It's a one-way street, you can set metadata via `exiv2` in the
sidecar, no way back.
* Although darktable *seems* to copy even those xmp metadata fields
to exported files it does not know (I've only tested this with
`Xmp.xmp.Label`), you might have to script the copying of metadata
from sidecars to exported images where this fails.
* Also, you can set new values to the extra metadata fields, but
selecting an image won't show the values of these fields set
previously.
* You cannot collect by these metadata fields in the "collect
images" module.
* Probably due to timing issues, images may appear as skulls and be
unaccessible until you restart darktable.
* Image IDs will change, because darktable needs to re-import the
sidecars after running the external tool, because it needs to read
the XMPs again and there's no Lua API to do this properly.
* During that re-import, images may disappear from the collection
(if you make `exiv2` change a metadata field defining the
collection), but still be part of the selection. So with your
next step you might act on invisible images and inadvertently harm
them.
* Working with the Lua API gave me the feeling of handling something
very fragile. I don't know whether it's a smart move to build
anything on it.
Maybe this script helps you (or someone else) as a starting point, it
could be easily extended to offer more useful metadate fields, but I
believe it's a crappy approach. It will not be possible to easily
extend this to solve the points above. Reasons are, among many
others, that Lua is the language it is, and just not suitable for some
necessary tasks, such as writing a parser for the data returned by an
external tool such as `exiv2`. Or that darktable contains a lot of
hard coded SQL statements, which makes it practically impossible to
use Lua to extend, e.g., the collection module. Achieving such
flexibility would probably require a complete redesign of the
lighttable portion of darktable.
Sorry, I will not extend or maintain this script, it was rather a
small learning project for myself.
Of course, there's still the chance that one of the devs will just
implement directly in darktable what you have asked for... Good luck!
Cheers
Stefan
--
http://stefan-klinger.de o/X
I prefer receiving plain text messages, not exceeding 32kB. /\/
\
____________________________________________________________________________
darktable user mailing list
to unsubscribe send a mail to [email protected]--[[ Note: The intention of this script is not to use `exiv2` to
extend darktable's metadata editor, but rather to have a look at
Lua, and to explore darktable's Lua interface. ]]
local dt = require "darktable"
local debugging = false
--[[ call `fun(...)` if `debugging` is true. ]]
local function ifDebug(fun, ...)
if debugging then fun(...) end
end
--[[ Append all following arguments to the table given as first
argument. ]]
local function append(t, ...)
for _, v in pairs({...}) do
table.insert(t, v)
end
end
--[[ Lua provides no interface to `execve`. So we must go through
`os.execute`, which takes one shell command as string, and thus
requires special care when quoting arguments (xkcd.com/327).
From a list (Lua: table) of arguments as one would pass them to
execve(2), create a command string to be passed to the shell via
`os.execute`, assuming that `'` is strong quoting as in
bash(1). ]]
local function mkCmdString(argv)
local words = {}
for _, a in pairs(argv) do
append(words, "'" .. a:gsub("'", "'\\''") .. "'")
end
return table.concat(words, ' ')
end
-- Definition of GUI elements to construct command line arguments
-- ==============================================================
-- root widget of the module
local libWidget = dt.new_widget("box"){ orientation = "vertical" }
-- entry field for Xmp.xmp.Label
local labelWidget = dt.new_widget("entry"){
text = '',
placeholder = '(label)',
editable = true,
tooltip = 'Xmp.xmp.Label'
}
-- entry field for Xmp.dc.publisher
local publisherWidget = dt.new_widget("entry"){
text = '',
placeholder = '(publisher)',
editable = true,
tooltip = 'Xmp.dc.publisher'
}
-- add entry fields to root widget
append(libWidget, labelWidget, publisherWidget)
--[[ Helper function to encode string arguments for `exiv2` commands.
FIXME: to do this properly, understand precisely when `exiv2`
commands need quotes around their arguments, and how they are
interpreted. E.g.
$ exiv2 '-M del Xmp.dc.publisher' \
'-M set Xmp.dc.publisher ""foo"bar"' \
"$someimage"
sets the publisher to `"foo"bar`, no shit!
NOTE: `exiv2` may not be the right tool to manipulate XMP files,
Check each use case from the command line first. E.g., I've
failed to make it set the Xmp.xmp.Rating on an XMP file, but it
works happily on a raw NEF file. ]]
local function exivStr(val)
return '"' .. val .. '"'
end
--[[ This is called to execute the `exiv2` command. It collects the
data from the GUI widgets, constructs shell commands from that
data and the names of the sidecar files of the selected images,
and runs them. If a command is successful, the image is deleted
from the database, and reimported.
FIXME: When reimporting images, they will get a new image ID in
the database. This is not the same as reloading a sidecar that
has changed when darktable starts. Sometimes images disappear (a
skull shows up), I don't know why.
FIXME: When a change in metadata excludes the image from the
collection, it is still in the selection, and further operations
will act on it! ]]
function runCommand()
-- abort if nothing is selected
if not dt.gui.action_images[1] then
dt.print('Nothing is selected')
return nil
end
-- first words of the command
local cmd = {
--'/usr/bin/printf', '> %s\\n', -- for debugging, prints command
'exiv2'
}
-- collect data from widgets, add arguments to the command
local nothingToDo = true -- falsified by any set entry
if labelWidget.text ~= '' then
nothingToDo = false
append(cmd, '-Mset Xmp.xmp.Label ' .. exivStr(labelWidget.text))
end
if publisherWidget.text ~= '' then
nothingToDo = false
append(cmd,
'-Mdel Xmp.dc.publisher',
'-Mset Xmp.dc.publisher ' .. exivStr(publisherWidget.text)
)
end
-- consistency check
if nothingToDo then
dt.print('Nothing to do')
return nil
end
--[[ Now the only part missing is the sidecar as final argument.
Depending on use case, one may add all images from the
selection and call the command once, or iterate over the
images and issue individual calls. I'll do the latter
here. ]]
--[[ DT needs to re-import images. Need to constuct new
selection. API unsatisfactory. ]]
local newSelection = {}
for _, img in pairs(dt.gui.action_images) do
-- Finally, add the path to the sidecar file as final argument
local oneshotCmd = { table.unpack(cmd) } -- shallow copy
append(oneshotCmd, img.sidecar)
-- Now run the command. If successful, reload the sidecar.
local cmdString = mkCmdString(oneshotCmd)
if (os.execute(cmdString)) then
ifDebug(print, '>>> externalToolFoo ok: ' .. cmdString)
-- Remove image, but memorize its path.
local path = img.path .. '/' .. img.filename
dt.database.delete(img)
--[[ FIXME: Sometimes images disappear. I cannot
reproduce this reliably. Adding a delay between
`img:delete` and `dt.database.import` does NOT help,
and would also be quite a nuisance. ]]
--dt.control.sleep(250)
local imported = dt.database.import(path)
if imported then
ifDebug(print, '>>> externalToolFoo imported: ' .. path)
table.insert(newSelection, imported)
else
print('>>> externalToolFoo FAILED to import: ' .. path)
dt.print('FAILED to import image')
end
else
print('>>> externalToolFoo FAILED to run: ', cmdString)
end
end
-- use new selection
--dt.gui.action_images = newSelection -- has no effect
dt.gui.selection(newSelection) -- may be deprecated
end
-- add run button to root widget
append(libWidget, dt.new_widget("button") {
label = 'run exiv2',
clicked_callback = runCommand
})
-- add lib to darktable
dt.register_lib(
'exiv2 runner',
'exiv2 runner',
true, -- expandable
false, -- resetable
{[dt.gui.views.lighttable] =
{"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 500}},
libWidget,
nil,-- view_enter
nil -- view_leave
)
ifDebug(print, 'externalToolFoo Loaded')