darktable page lede image
darktable page lede image

Scripting com Lua

Capítulo 9. Scripting com Lua

O darktable vem com uma interface de scripting versátil para melhoria de funcionalidade.

9.1. Uso de Lua

Lua pode ser usada para definir ações que o darktable executa sempre que um evento específico é acionado. Um exemplo pode ser chamar uma aplicação externa durante a exportação para aplicar passos de processamento adicional fora do darktable.

O darktable usa Lua, que é um projeto independente fundado em 1993, provendo uma linguagem de scripting poderosa, rápida, leve, e embarcável. Lua é amplamente usada em aplicações open source, em programas comerciais, e para programação de jogos.

O darktable usa Lua versão 5.2. A descrição dos princípios e sintaxe de Lua fica fora do escopo deste manua. Para uma introdução detalhada veja o Manual de referência de Lua.

9.1.1. Princípios básicos

Ao iniciar, o darktable executará dois scripts Lua:

  • um script chamado luarc em $DARKTABLE/share/darktable

  • um script chamado luarc na pasta de configuração do usuário

$DARKTABLE é usado aqui para representar a pasta de instalação do darktable no seu sistema.

Este é o único momento que o darktable executará scripts Lua por conta própria. O script pode registrar callbacks para realizar ações em vários eventos do darktable. Este mecanismo de callback é a maneira primária de disparar ações Lua.

9.1.2. Um exemplo simples em Lua

Vamos começar com um exemplo simples. Vamos escrever algo no console. Crie um arquivo chamado luarc na pasta de configuração do darktable (normalmente ~/.config/darktable/) e adicione a seguinte linha ao arquivo:

print("Hello World !")

Inicie o darktable e você verá a sentença Hello World ! escrita no console. Nada demais, mas é um começo...

Neste ponto, não há nada específico do darktable no script. Nós simplesmente eusamos a função padrão print para mostrar uma string. Isto é bom, mas podemos fazer mais. Para acessar a API do darktable você precisa primeiro chamar require e guardar o objeto retornado em uma variável. Uma vez que isto tenha sido feito, você poderá acessar a API como subcampos do objeto retornado. Tudo isto está documentado no manual de referência da API Lua do darktable (veja Seção 9.2, “API Lua”).

local darktable = require "darktable"
darktable.print_log("Hello World !")

Rode o script... E nada acontece. A função darktable.print_error é como print, mas só mostrará a mensagem se você tiver habilitado lua traces com -d lua na linha de comando. Esta é a maneira recomendada de fazer traces em um script Lua do darktable.

9.1.3. Mostrando imagens etiquetadas

Este primeiro exemplo nos mostrou o básico de Lua e permitiu verificarmos como tudo funcionou perfeitamente. Faremos algo um pouco mais complexo. Tentaremos escrever a lista de imagens que tem uma etiqueta vermelha anexada. Mas antes de mais nada, o que é uma imagem?

local darktable = require "darktable"
local debug = require "darktable.debug"
print(darktable.debug.dump(darktable.database[1]))

Executar o código acima produzirá muita saída. Nós a examinaremos em um momento, mas primeiro veremos o próprio código.

Sabemos sobre precisar requerer darktable . Aqui, precisamos separadamente requerer derktable.debug que é uma seção opcional da API que provê funções auxiliares para ajudar a depurar scripts Lua.

darktable.database é uma tabela provida pela API que contém todas as imagens na base de dados (atualmente visíveis ou não, duplicadas ou não...). Cada entrada na base de dados é um objeto imagem. Objetos imagem são objetos complexos que permitem que você manipule sua imagem de várias maneiras (é completamente documentado na seção types_dt_lua_image_t do manual da API). Para mostrar nossas imagens, precisamos usar darktable.debug.dump, que é uma função que tomará qualquer coisa como parâmetro e recursivamente mostrará seu conteúdo. Como imagens são objetos complexos que referenciam indiretamente outros objetos, o resultado da saída é enorme. A seguir está um exemplo reduzido dessa saída.



          toplevel (userdata,dt_lua_image_t) : /images/100.JPG publisher (string) : "" path (string) : "/images" move (function) exif_aperture (number) : 2.7999999523163 rights (string) : "" make_group_leader (function) exif_crop (number) : 0 duplicate_index (number) : 0 is_raw (boolean) : false exif_iso (number) : 200 is_ldr (boolean) : true rating (number) : 1 description (string) : "" red (boolean) : false get_tags (function) duplicate (function) creator (string) : "" latitude (nil) blue (boolean) : false exif_datetime_taken (string) : "2014:04:27 14:10:27" exif_maker (string) : "Panasonic" drop_cache (function) title (string) : "" reset (function) create_style (function) apply_style (function) film (userdata,dt_lua_film_t) : /images 1 (userdata,dt_lua_image_t): .toplevel [......] exif_exposure (number) : 0.0062500000931323 exif_lens (string) : "" detach_tag (function): toplevel.film.2.detach_tag exif_focal_length (number) : 4.5 get_group_members (function): toplevel.film.2.get_group_members id (number) : 1 group_with (function): toplevel.film.2.group_with delete (function): toplevel.film.2.delete purple (boolean) : false is_hdr (boolean) : false exif_model (string) : "DMC-FZ200" green (boolean) : false yellow (boolean) : false longitude (nil) filename (string) : "100.JPG" width (number) : 945 attach_tag (function): toplevel.film.2.attach_tag exif_focus_distance (number) : 0 height (number) : 648 local_copy (boolean) : false copy (function): toplevel.film.2.copy group_leader (userdata,dt_lua_image_t): .toplevel

        

Como podemos ver, uma imagem tem um grande número de campos que proveem todo tipo de informação sobre ela. Estamos interessados na etiqueta vermelha. Este campo é um booleano, e a documentação nos diz que pode ser escrito. Agora só precisamos encontrar as imagens com este campo e mostrá-las.

darktable = require "darktable"
for _,v in ipairs(darktable.database) do
  if v.red then
    print(tostring(v))
  end
end

Este código deve ser bem simples de entender neste ponto, mas contém alguns aspectos interessantes sobre Lua que vale a pena destacar:

  • ipairs é uma função padrão Lua que itera por todos os índices numéricos em uma tabela. Usamos aqui porque darktable.database tem índices não-numéricos que são funções para manipular a própria base de dados (adicionar ou remover imagens, por exemplo).

  • Iterar por toda a tabela returna tanto a chave como o valor usado. É convencional em Lua usar uma variável chamada _ para guardar valores que não nos interessam.

  • Note que usamos a função padrão Lua tostring, e não a específica do darktable, darktable.debug.dump . A função padrão retornará um nome para o objeto enquanto a função de depuração mostrará seu conteúdo. A função de depuração pode dar detalhes demais aqui. Novamente, é uma excelente ferramenta para depuração, mas não deve ser usada pra mais nada.

9.1.4. Adicionando um atalho simples

Até agora, nossos scripts só fizeram coisas durante a inicialização. isto é limitado e não nos permite reagir a ações reais do usuário. Para fazer algo mais avançado precisamos registrar uma função que será chamada em um dado evento. O evento mais comum para reagir é um atalho de teclado.

darktable = require "darktable"

local function hello_shortcut(event, shortcut)
darktable.print("Hello, I just received '"..event..
       "' with parameter '"..shortcut.."'")
end

darktable.register_event("shortcut",hello_shortcut,
       "A shortcut that print its parameters")

Agora inicie o darktable, vá a preferências => atalhos => lua => Um atalho que mostra seus parâmetros , associe-o com um atalho e use-o. Você deverá ver uma mensagem mostrada na tela.

Vamos olhar o código em detalhes. Primeiro definimos uma função com dois parâmetros. Estes parâmetros são srtings. O primeiro é o tipo de evento disparado ( "shortcut" ) e o segundo é que atalho especificamente ( "Um atalho que mostra seus parâmetros" ). A função chama darktable.print que imprimirá a mensagem como sobreposição na janela principal.

Uma vez que a função tenha sido definida, nós a registramos como callback de atalho. Para isto chamamos darktable.register_event que é uma função genérica para todos os tipos de evento. Dizemos a ela que estamos registrando um evento atalho, e então damos o callback a chamar e finalmente, damos a string que deve ser usada para o atalho na janela de preferências.

Vamos tentar um atalho um pouco mais interativo. Este irá olhar as imagens em que o usuário está atualmente interessado (selecionadas ou sob o mouse) e aumentará sua classificação.

darktable = require "darktable"

darktable.register_event("shortcut",function(event,shortcut)
    local images = darktable.gui.action_images
    for _,v in pairs(images) do
      v.rating = v.rating + 1
    end
    end,"Increase the rating of an image")      

Neste ponto, a maioria deste código deveria ser auto-evidente. Só algumas notas:

Se você selecionar uma imagem e pressionar seu atalho algumas vezes, ele funcionará corretamente na primeira vez, mas quando chegar a cinco estrelas, o darktable começará a mostrar o seguinte erro no console:



          LUA ERROR : rating too high : 6 stack traceback: [C]: in ? [C]: in function '__newindex' ./configdir/luarc:10: in function <./configdir/luarc:7> LUA ERROR : rating too high : 6

        

Esta é a maneira como Lua reporta erros. Tentamos determinar uma classificação de 6 a uma imagem, mas classificações só podem ir até 5. Seria trivial adicionar uma verificação, mas vamos seguir o caminho mais complicado e, ao invés disso, capturar o erro.

darktable.register_event("shortcut",function(event,shortcut)
    local images = darktable.gui.action_images
    for _,v in pairs(images) do
      result,message = pcall(function()
        v.rating = v.rating + 1
        end)
      if not result then
        darktable.print_error("could not increase rating of image "..
          tostring(v).." : "..message)
      end
    end
end,"Increase the rating of an image")

pcall executará seu primeiro argumento e capturará qualquer exceção que ele arremesse. Se não há exceções ele retornará true mais o resultado retornado pela função; se tiverhavido exceções, ele retornará false e a mensagem de erro da exceção. Simplesmente testamos os resultados e os mostramos no console...

9.1.5. Exportando imagens com Lua

Aprendemos como usar Lua para adaptar o darktable a nosso fluxo de trabalho particular. Agora veremos como usar Lua para facilmente exportar imagens. O darktable pode exportar imagens para alguns serviços online, mas sempre há outros. Se você não consegue fazer upload de uma imagem para um serviço pela linha de comando, pode usar Lua para integrá-lo à interface de usuário do darktable.

Neste exemplo vamos usar Lua para exportar usando o comando scp. Um novo armazenamento surgirá na interface do darktable que exportará imagens a uma máquina remota usando o mecanismo ssh.

darktable = require "darktable"

darktable.preferences.register("scp_export","export_path",
  "string","target SCP path",
  "Complete path to copy to. Can include user and hostname","")

darktable.register_storage("scp_export","Export via scp",
  function( storage, image, format, filename,
     number, total, high_quality, extra_data)
    if not darktable.control.execute("scp "..filename.." "..
      darktable.preferences.read("scp_export",
         "export_path","string")) then
      darktable.print_error("scp failed for "..tostring(image))
    end
end)

darktable.preferences.register adicionará uma nova preferência ao menu de preferências do darktable. scp_export e export_path permitem identificar unicamente nossa preferência. Estes campos são reusados quando lemos o valor das preferências. O campo string diz ao motor Lua que a preferência é uma string. Poderia ser um inteiro, um nome de arquivo, ou qualquer dos tipos relacionados com types_lua_pref_type no manual da API Lua. Quando temos uma etiqueta para preferência no menu de preferências, as janelas de dica quando o mouse passar pelo valor, e um valor default.

darktable.register_storage é a chamada que registra um novo armazenamento. O primeiro argumento é o nome do armazenamento, o segundo é um rótulo que será mostrado na interface de usuário e o último é uma função a ser chamada em cada imagem. Esta função tem vários parâmetros, mas filename é o único que usamos neste exemplo. Ele contém o nome de um arquivo temporário onde a imagem é exportada pelo darktable.

Este código funciona, mas tem algumas limitações. Este é só um exemplo simples, afinal de contas:

  • Usamos as preferências para configurar o caminho destino. Seria melhor adicionar um elemento para a interface de exportação do darktable. Vamos detalhar como fazer isso na próxima seção.

  • Não verificamos o valor de retorno do scp. Este comando pode falhar, em particular se o usuário não tiver configurado corretamente a preferência.

  • Este script não pode ler entrada de dados do usuário. O scp remoto precisa fazer cópia sem usar senha. O scp não pode receber uma senha facilmente, então deixamos da maneira como está.

  • Nenhuma mensagem é mostrada depois que o exemplo termina, somente a barra de progresso no canto inferior esquerdo diz ao usuário que o trabalho terminou.

  • Usamos corouteine.yield para chamar um programa externo. O usual os.execute bloquearia outros códigos Lua.

9.1.6. Construindo elementos de interface com o usuário

Nosso exemplo anterior é um pouco limitado. Em particular, o uso de uma preferência para o caminho de exportação não é bom. Podemos fazer melhor adicionando elementos à interface de usuário no diálogo de exportação.

Elementos da interface são criados com darktable_new_widget. Esta função toma um tipo de widget como parâmetro e retorna um novo objeto correspondente ao widget. Você pode definir vários campos nesse widget para ajustar seus parâmetros. Você então usará esse objeto como parâmetro para várias funções que o adicionarão à interface do darktable. O exemplo simples a seguir adiciona uma lib na vista da mesa de luz com um rótulo simples.

local my_label = darktable.new_widget("label")
      my_label.label = "Hello, world !"


dt.register_lib("test","test",false,{
    [dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER",20},
    },my_label)    

Há um truque sintático interessante que torna código de elementos da interface mais fácil de ler e escrever. Você pode chamar estes objetos como funções com uma tabela de chaves como argumento. Isto permite que o exemplo a seguir funcione. Ele cria um widget container com dois subwidgets. Um rótulo e uma entrada de texto.

	local my_widget = darktable.new_widget("box"){
		orientation = "horizontal",
		darktable.new_widget("label"){ label = "here => " },
		darktable.new_widget("entry"){ tooltip = "please enter text here" }
	}

Agora que sabemos isso, vamos melhorar um pouco nosso script.

darktable = require "darktable"

local scp_path = darktable.new_widget("entry"){
  tooltip ="Complete path to copy to. Can include user and hostname",
  text = "",
  reset_callback = function(self) self.text = "" end
}


darktable.register_storage("scp_export","Export via scp",
  function( storage, image, format, filename,
     number, total, high_quality, extra_data)
    if not darktable.control.execute("scp "..filename.." "..
      scp_path.text
    ) then
      darktable.print_error("scp failed for "..tostring(image))
    end
    end,
    nil, --finalize
    nil, --supported
    nil, --initialize
    darktable.new_widget("box") {
    orientation ="horizontal",
    darktable.new_widget("label"){label = "target SCP PATH "},
    scp_path,
})

9.1.7. Compartilhando scripts

Até agora, nosso código Lua estava em luarc. É uma boa maneira de desenvolver seu script mas não muito prática para distribuição. Precisamos transformar isto em um módulo Lua de verdade. Para fazer isso, gravamos o código em um arquivo separado (scp-storage.lua no nosso caso):

--[[
SCP STORAGE
a simple storage to export images via scp

AUTHOR
Jérémy Rosen (jeremy.rosen@enst-bretagne.fr)

INSTALLATION
* copy this file in $CONFIGDIR/lua/ where CONFIGDIR
is your darktable configuration directory
* add the following line in the file $CONFIGDIR/luarc
  require "scp-storage"

USAGE
* select "Export via SCP" in the storage selection menu
* set the target directory 
* export your images

LICENSE
GPLv2

]]
darktable = require "darktable"
darktable.configuration.check_version(...,{2,0,0})

local scp_path = darktable.new_widget("entry"){
  tooltip ="Complete path to copy to. Can include user and hostname",
  text = "",
  reset_callback = function(self) self.text = "" end
}


darktable.register_storage("scp_export","Export via scp",
  function( storage, image, format, filename,
     number, total, high_quality, extra_data)
    if darktable.control.execute("scp "..filename.." "..
      scp_path.text
    ) then
      darktable.print_error("scp failed for "..tostring(image))
    end
    end,
    nil, --finalize
    nil, --supported
    nil, --initialize
    darktable.new_widget("box") {
    orientation ="horizontal",
    darktable.new_widget("label"){label = "target SCP PATH "},
    scp_path,
})

O darktable procurará scripts (usando as regras normais de Lua) nas pastas padrão, além de $CONFIGDIR/lua/?.lua . Então seu script pode ser chamado simplesmente adicionandl require "scp-storage" no arquivo luarc. Algumas notas mais...

  • A função darktable.configuration.check_version verificará a compatibilidade para você. O ... será transformado no nome de seu script e {2,0,0} é a versão da API com a qual você testou seu script. Você pode adicionar múltiplas versões de API se quiser atualizar seu script para múltiplas versões do darktable.

  • Certifique-se de declarar todas as suas funções como local para não poluir o espaço de nomes geral.

  • Certifique-se de não deixar prints de depuração em seu código. darktable.print_error, em particular, permite que você deixe prints de depuração no seu código final sem atrapalhar o console.

  • Você é livre para escolher qualquer licença para seu script, mas os scripts que são enviados ao website do darktable devem ser GPLv2.

Uma vez que tiver preenchido todos os campos, verificado seu código, você pode enviá-lo para nossa página de scripts here.

9.1.8. Chamando Lua através do DBus

É possível mandar um comando Lua para o darktable através de sua interface DBus. O método org.darktable.service.Remote.Lua toma um parâmetro simples do tipo string, que é interpretado como um comando Lua. O comando será executado no contexto Lua atual, e deveria retornar nil ou uma string. O resultado será passado de volta como resultado do método DBus.

Se a chamada Lua resultar em erro, a chamada de método DBus retornará um erro org.darktable.Error.LuaError com a mensagem de erro de Lua como mensagem anexada no erro DBus.

9.1.9. Usando o darktable a partir de um script Lua

Atenção: esta funcionalidade é muito experimental. É sabido que muitos elementos não funcionam no modo biblioteca ainda. Teste cuidadoso é altamente recomendado.

A interface Lua permite que você use o darktable a partir de qualquer script Lua. O darktable será carregado como biblioteca e proverá a maioria da API de Lua (o darktable é configurado no modo headless, então as funções relacionadas a interface gráfica não estarão disponíveis).

Como um exemplo, o programa a seguir mostra a lista de todas as imagens em sua biblioteca:

#!/usr/bin/env lua
package = require "package"
package.cpath=package.cpath..";./lib/darktable/lib?.so"

dt = require("darktable")(
"--library", "./library.db",
"--datadir", "./share/darktable",
"--moduledir", "./lib/darktable",
"--configdir", "./configdir",
"--cachedir","cachedir",
"--g-fatal-warnings")

require("darktable.debug")

for k,v in ipairs(dt.database) do
	print(tostring(v))
end

Note que a terceira linha aponta para a localização do arquivo libdarktable.so.

Note também que a chamada a require returna uma função que pode ser chamada somente uma vez e permite que você defina o parâmetro de linha de comando do darktable. O parâmetro :memory: para --library é útil aqui se você não quiser trabalhar em sua biblioteca pessoal.