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')

Reply via email to