darktable page lede image
darktable page lede image

Scripting con Lua

Capítulo 9. Scripting con Lua

darktable viene con una versátil interfaz de scripting para mejoras en sus funcionalidades.

9.1. Uso de Lua

Lua puede ser utilizado para definir las acciones que darktable realizará, cuando un evento específico sea desencadenado. Un ejemplo puede ser una llamada a una aplicación externa durante la exportación de un archivo para aplicar pasos adicionales de procesamiento fuera de darktable.

darktable utiliza Lua, el cual es un proyecto independiente fundado en 1993, el cual provee un lenguaje de scripting poderoso, rápido, ligero e integrado. Lua es ampliamente utilizado en muchas aplicaciones de código abierto, en programas comerciales y por programadores de video juegos.

darktable utiliza la versión 5.2 de Lua. Describir los principios y sintaxis de Lua está mas allá de nuestro objetivo dentro de este manual de usuarios. Para una introducción detallada vea el manual de referencias de Lua.

9.1.1. Principios básicos

Al inicio, darktable correrá dos scripts de Lua automáticamente.

  • un script llamado luarc en $DARKTABLE/share/darktable

  • un script llamado luarc en el directorio de configuración del usuario

$DARKTABLE es utilizado aquí para representar el directorio de su sistema de instalación de darktable

Esta es la única vez que darktable correrá un script de Lua por si mismo. El script puede registrar callbacks para realizar acciones sobre varios eventos de darktable. Este mecanismo de llamadas es la forma principal de activar las acciones lua.

9.1.2. Un simple ejemplo de lua

Comencemos con un ejemplo simple. Imprimiremos algunos códigos en la consola. Cree un archivo llamado luarc en el directorio de configuración de darktable (usualmente ~/.config/darktable/) y agregue la siguiente línea:

print("Hola Mundo !")

Inicie darktable y verá la frase Hola Mundo ! impresa en la consola. Nada muy elaborado pero es un inicio...

En este punto, no hay nada específico para darktable en el script. Simplemente utilizamos una función de print para imprimir una cadena. Eso está bien y todo, pero podemos hacer algo mejor que eso. Para acceder a la API de darktable primero necesita requerirlo y almacenar el objeto regresado en una variable. Una vez realice esto podrá acceder a la API de darktable como un sub-campo del objeto retornado. Todo esto está documentado en el manual de referencias del API de Lua (vea el Sección 9.2, “API Lua”).

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

Corra el script ... y nada sucede. La función darktable.print_error es como print pero solo imprimirá el mensaje si tiene activadas las rutas lua con -d lua en la línea de comando. Esta es una forma recomendada de realizar trazos en un script lua de darktable.

9.1.3. Imprimiendo imágenes etiquetadas

Este primer ejemplo nos muestra lo básico de lua y nos permite verificar que todo funcione propiamente. Hagamos algo un poco más complejo. Intentemos imprimir la lista de imágenes que tienen una etiqueta roja adjunta a ellas. Pero primero, ¿Qué es una imagen?

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

Corriendo el código anterior se producirá una gran salida. Ya la veremos en un momento, pero primero, demos un vistazo al código en sí.

Conocemos los requerimientos de darktable . Aquí, necesitamos requerir por separado darktable.debug la cual es una sección opcional de la API que provee funciones de ayuda para depurar los scripts lua.

darktable.database es una tabla provista por la API que contiene todas las imágenes en la base de datos (visible o no, duplicada o no...). Cada entrada en la base de datos es un objeto de imagen. Los objetos de imágenes son objetos complejos que le permiten manipular su imagen de varias formas (todo esto está documentado en la sección types_dt_lua_image_t del manual de la API). Para mostrar nuestras imágenes, utilizamos darktable.debug.dump la cual es una función que tomará todo lo que sea un parámetro y mostrará recursivamente su contenido. Ya que las imágenes son objetos complejos que hacen referencia indirecta a otros objetos complejos, la salida resultante es inmensa. Debajo encontrará un ejemplo seccionado de la salida.



          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, una imagen tiene una gran cantidad de campos que proveer todo tipo de información sobre ella. Nos interesa la etiqueta roja. Este campo es un bolean, y la documentación nos indica que puede ser escrito. Ahora solo necesitamos encontrar todas las imágenes con dicho campo e imprimirlas.

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

Este código es bastante sencillo de entender en este punto, pero contiene algunos aspectos interesantes sobre lua que vale la pena resaltar:

  • ipairs es una funcionalidad estándar de lua que iterará todos los indices numéricos de una tabla. Lo usamos aquí porque darktable.database tiene indices no-numéricos que son funciones para manipular la base de datos (por ejemplo, agregando o eliminando imágenes).

  • Realizar la iteración de una table retornará tanto las llaves como los valores utilizados. Es convencional utilizar en lua una variable llamada _ para almacenar los valores que no nos interesan.

  • Note que utilizamos la función estándar de lua tostring y no la específica de darktable darktable.debug.dump. La función estándar retornará un nombre para el objeto, donde la función de depurar imprimirá el contenido. La función de depurado sería demasiado verbosa para ser utilizada aquí. De nuevo, es una gran herramienta de depurado pero no debe ser utilizada para mas nada.

9.1.4. Agregando un simple un atajo de teclado

Hasta ahora, todos nuestros scripts han realizado algo durante el inicio. Esto da un uso limitado y no nos permite reaccionar a acciones reales del usuario. Pare realizar cosas mas avanzadas necesitamos registrar una función que será llamada en un evento dado. El evento más común al cual reaccionar es un atajo 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")
      

Ahora inicie darktable, vaya a preferencias => atajo => lua => Un atajo que imprime sus parámetros asigne un atajo y pruebe. Debería tener un buen mensaje impreso en la pantalla.

Demos un vistazo al código en detalle. Primero definimos una función con dos parámetros. Estos parámetros son cadenas. El primero es el tipo de evento que será ejecutado ( "atajo" ) y el segundo es el atajo en específico ( "Un ajato que imprime sus parámetros" ). La función misma se llama darktable.print la cual imprimirá el mensaje de forma solapada sobra la ventana principal.

Una vez que la función sea definida, lo registraremos como un atajo de teclado. Para realizar esto, llamaremos a darktable.register_event la cual es una función genérica para todos los tipos de eventos. Le diremos que estamos registrando un atajo como evento, luego le daremos la llamada y por último, le indicaremos la cadena que se utilizará para describir el atajo en la ventana de preferencias.

Intentemos con un atajo un poco mas interactivo. Este buscará el usuario que está interesado actualmente en las imágenes existentes (seleccionadas o marcadas con el puntero del ratón) e incrementará su puntaje.

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

En este punto, la mayoría de este código debería ser auto explicativo. Solo un par de notas:

  • En vez de declarar una función y hacerle referencia, la declararemos directamente en la llamada a darktable.register_event esto es estrictamente equivalente pero un poco más compacto.

  • image.rating es el campo de cualquier imagen que otorga el puntaje (entre 0 y 5 estrellas, -1 significa rechazada).

  • darktable.gui.action_images es una tabla que contiene todas las imágenes de interés. darktable actuará sobre las imágenes seleccionadas si cualquier imagen es seleccionada, y sobre otras imágenes con el puntero del ratón sobre ellas si ninguna imagen ha sido seleccionada. Esta función le permite seguir fácilmente la lógica en lua de la interfaz de darktable.

Si selecciona una imagen y presiona su atajo un par de veces, funcionará correctamente al principio, pero luego de que haya llegado a las cinco estrellas, darktable comenzará a mostrá un mensaje de error en la consola:



          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 es la forma que tiene lua para reportar errores. Hemos estado tentados a ajustar un puntaje de 6 a una imagen, pero un puntaje solo puede subir hasta 5. Sería trivial agregar una revisión, pero en cambio vayamos a la forma complicada y obtengamos el error.

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 correrá su primer argumento y atajará cualquier excepción que sea lanzada. Si no hay excepciones devolverá un true (verdadero) además de cualquier resultado retornado por la función; si hay una excepción retornará false (falso) y el mensaje de error de la excepción. Simplemente probaremos estos resultados y los imprimiremos en la consola...

9.1.5. Exportando imágenes con Lua

Hemos aprendido a utilizar lua para adaptar darktable a nuestro flujo de trabajo particular, demos un vistazo a como utilizar lua para exportar imágenes fácilmente. darktable puede exportar imágenes de forma fácil a varios servicios en línea pero siempre hay mas. Si usted es capaz de subir una imagen a un servicio vía línea de comandos, entonces puede utilizar lua para integrarlo a la interfaz de usuario de darktable.

En este nuevo ejemplo utilizaremos lua para exportar vía scp. Un nuevo almacenamiento aparecerá en la interfaz de darktable, la cual exportará las imágenes a un objetivo remoto vía el mecanismo de copia 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 coroutine.yield("RUN_COMMAND","scp "..filename.." "..
      darktable.preferences.read("scp_export",
         "export_path","string")) then
      darktable.print_error("scp failed for "..tostring(image))
    end
end)

darktable.preferences.register agregará una nueva preferencia al menú de preferencias de darktable. scp_export y export_path le permitirán identificar individualmente nuestras preferencias. Estos campos son reutilizados cuando leemos el valor de la preferencia. EL campo de string le indica al motor de lua que la preferencia es una cadena (string). También puede ser un entero, un nombre de archivo o cualquiera de los tipos detallados en el manual de API relacionados a types_lua_pref_type. Entonces tendremos la etiqueta para las preferencias en el menú de preferencias, la caja de consejos al pasar el puntero del ratón sobre un valor y el valor por defecto.

darktable.register_storage es la llamada que realmente registra un nuevo almacenamiento. El primero argumento es el nombre del almacenamiento, el segundo es la etiqueta que se mostrará en la Interfaz y el último es la función de llamada de cada imagen. Esta función tiene muchos mas parámetros, pero filename (nombre de archivo) es la única que utilizaremos en este ejemplo. Esta contiene el nombre del archivo temporal donde la imagen fue exportada por el motor de darktable.

Este código funcionará pero tiene un par de limitaciones. Esto es un simple ejemplo después de todo:

  • Utilizamos preferencias para configurar la ruta del objetivo. Sería mucho mejor agregar un elemento a la Interfaz de exportado en darktable. Detallaremos como realizar esto en la próxima sección

  • Nosotros no revisamos el valor retornado del scp. Ese comando puede fallar, particularmente si el usuario no ha ajustado correctamente la preferencia.

  • Este script no puede leer las entradas del usuario. El scp remoto debe utilizar una copia menor de la clave. Scp no puede proveer una clave fácilmente, así que lo dejaremos tal como está

  • No se mostrará un mensaje una vez que el ejemplo se haya realizado, solo la barra de progreso a la izquierda le indicará al usuario que el trabajo ha sido realizado.

  • Utilizamos coroutine.yield para llamar a un programa externo. El código normal os.execute bloquearía que otros códigos lua se ejecuten.

9.1.6. Construyendo elementos de la Interfaz de Usuario

Nuestro ejemplo anterior fue un poco limitado. Particularmente, el uso de una preferencia para una ruta de exportado no fue muy agradable. Podemos hacer algo mejor que eso al agregar elementos a la interfaz de usuario dentro del diálogo de exportar.

Los elementos de la interfaz son creados con la función darktable_new_widget. Esta función toma un tipo de widget como un parámetro y retorna un nuevo objeto que le corresponderá a dicho widget. Puede ajustar varios campos para dicho widget para ajustar sus parámetros. Entonces podrá utilizar objetos como parámetros de varias funciones, las cuales se agregarán a la Interfaz de darktable. El siguiente ejemplo simple agrega una lib en la vista de mesa de luz con una etiqueta sencilla

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)
    

Aquí tiene un buen truco de sintaxis para que el código de los elementos de su Interfaz se pueda leer y escribir más fácilmente. Puede llamar estos objetos como funciones con una tabla de valores clave como un argumento. Esto permite que el siguiente ejemplo pueda funcionar. Crea un widget contenedor con dos sub-widgets. Una etiqueta y un campo de 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" }
	}

Ahora que sabemos esto, mejoremos un poco nuestro 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 coroutine.yield("RUN_COMMAND","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. Compartiendo scripts

Hasta ahora, nuestro código lua estaba en luarc. Esa es una buena forma de desarrollar su script pero no muy práctico para ser distribuido. Necesitamos convertir esto en un módulo propio de lua. Para hacerlo, guardamos el código en un archivo separado (scp-storage.lua en nuestro 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 coroutine.yield("RUN_COMMAND","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,
})

darktable will look for scripts (following the normal lua rules) in the standard directories plus $CONFIGDIR/lua/?.lua . So our script can be called by simply adding require "scp-storage" in the luarc file. A couple of extra notes...

  • La función darktable.configuration.check_version revisará la compatibilidad por usted. El ... se convertirá en el nombre de su script {2,0,0} es la versión de API con la que ha probado su script. Puede agregar múltiples versiones de API si actualiza su script para múltiples versiones de darktable

  • Asegúrese de declarar todas sus funciones como local para no contaminar el nombre en general.

  • Asegúrese de no dejar impresiones del depurado en su código. darktable.print_error le permitirá particularmente, dejar impresiones de depuración en su código final sin perturbar la consola.

  • Es libre de escoger cualquier licencia para su script, pero los scripts que son cargados en el sitio web de darktable necesitan ser GPLv2.

Once you have filled all the fields, checked your code, you can upload it to our script page here.

9.1.8. Llamando Lua desde DBus

Es posible enviar un comando lua a darkable vía la interfaz del DBus. El método org.darktable.service.Remote.Lua toma una sola cadena de parámetro, la cua es interpretada como un comando lua. El comando será ejecutado en el contexto actual de lua y debería bien sea retornar un nil o una cadena. El resultado se devolverá como resultado del método DBus.

Si la llamada a Lua resulta en un error, la llamada del método DBus retornará un error org.darktable.Error.LuaError con el mensaje de error de lua como un mensaje adjunto al error de DBus.

9.1.9. Utilizando darktable desde un script de lua

Precaución: Esta propiedad es bastante experimental. Se sabe que varios elementos aún no funcionan en el modo de librería. Se recomienda tener mucho cuidado al realizar estas pruebas.

La interfaz de lua le permite utilizar darktable desde cualquier script lua. Esto cargará darktable como una librería y le proveerá con la mayoría de la API lua (darktable está configurado sin cabeceras, así que las funciones relacionadas a la interfaz no están disponibles).

Como ejemplo, el siguiente programa imprimirá la lista de todas las imágenes en su librería:

#!/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 la tercera línea apunta a la ubicación del archivo libdarktable.so.

Also note that the call to require returns a function that can be called only once and allows you to set darktable's command line parameter. The :memory: parameter to --library is useful here if you don't want to work on your personal library.