darktable page lede image
darktable page lede image

Créer des scripts avec Lua

Chapitre 9. Créer des scripts avec Lua

darktable possède une interface flexible pour l’écriture de scripts améliorant ses fonctionnalités.

9.1. Utilisation de Lua

Lua peut être utilisé pour définir des actions que darktable devra effectuer lorsqu’un événement particulier est déclenché. Lors de l’exportation de fichier on pourra par exemple faire appel à une application externe de manière à appliquer des étapes supplémentaires de traitement en dehors de darktable.

darktable utilise Lua, un projet indépendant lancé en 1993. Il procure un langage de script puissant, rapide, léger et intégrable. Lua est largement utilisé par de nombreuses applications à sources ouvertes, dans des programmes commerciaux et pour la programmation de jeux.

darktable utilise Lua version 5.2. La description des principes et de la syntaxe de Lua n'entre pas dans le cadre de ce manuel utilisateur. Pour une introduction détaillée voyez le Manuel de référence de Lua.

9.1.1. Principes de base

Au démarrage, darktable va lancer automatiquement deux scripts Lua :

  • un script appelé luarc se trouvant dans $DARKTABLE/share/darktable ;

  • un script appelé luarc se trouvant dans le répertoire de configuration de l’utilisateur.

$DARKTABLE représente le répertoire d’installation de darktable sur votre système.

C’est le seul moment où darktable lance des scripts Lua par lui-même. Les scripts peuvent déclarer des fonctions de rappel pour effectuer des actions liées à divers événements darktable. Ce mécanisme des fonctions de rappel est le principal moyen de déclencher des actions Lua.

9.1.2. Un simple exemple de script Lua

Commençons par un simple exemple. Nous allons imprimer du code sur la console. Créez un fichier nommé luarc dans le répertoire de configuration de darktable (habituellement ~/.config/darktable/) et ajoutez-y la ligne suivante :

print("Hello World !")

Démarrez darktable et vous verrez la phrase Hello World ! s'afficher sur la console. Rien d'extraordinaire mais c'est un début ...

À ce stade, il n'y a rien de spécifique à darktable dans le script. Nous utilisons simplement la fonction standard print pour afficher une chaîne de caractères. Voilà c'est sympa et tout, mais nous pouvons faire mieux que cela. Pour pouvoir accéder à l'API darktable vous devez tout d'abord la requérir et sauvegarder l'objet retourné dans une variable. Une fois ceci fait, vous pouvez accéder à l'API darktable comme un sous-champ de l'objet retourné. Tout ceci est documenté dans le manuel de référence de l'API Lua de darktable.

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

Lancez le script ... et il ne se passe rien. La fonction darktable.print_error est identique à la fonction print mais elle affichera le message seulement si vous avez activé les traces Lua avec la commande -d lua sur la ligne de commande. C'est ce qui est recommandé pour activer les traces dans un script Lua de darktable.

9.1.3. Affichage des images marquées

Ce premier exemple nous a montré les rudiments de Lua et nous a permis de vérifier que tout fonctionne correctement. Faisons quelque chose d'un peu plus complexe. Essayons d'afficher la liste des images marquées par un label rouge. Mais tout d'abord, qu'est-ce qu'une image?

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

L'exécution du code ci-dessus va produire un grand nombre de sorties. Nous allons les examiner dans un instant, mais tout d'abord, regardons le code lui-même.

Nous savons qu'il faut requérir darktable. Ici nous avons besoin de requérir séparément darktable.debug qui est une section optionnelle de l'API procurant des fonctions d'aide au débogage des scripts Lua.

darktable.database est une table fournie par l'API qui contient toutes les images de la base de données ( actuellement visibles ou non, clonées ou non ...). Chaque entrée dans la base de données est un objet image. Les objets image sont des objets complexes qui vous permettent de manipuler vos images de différentes façons (tout ceci est documenté dans la section types_dt_lua_image_t du manuel de l'API). Pour afficher nos images, nous utilisons darktble.debug.dump qui est une fonction prenant un objet quelconque en tant que paramètre et affiche récursivement son contenu. Puisque les images sont des objets complexes référençant indirectement d'autres objets complexes la sortie résultante est énorme. Vous trouverez ci-dessous en exemple un extrait de la sortie.



          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

        

Comme vous pouvez le voir, une image a un grand nombre de champs fournissant toutes sortes d'informations la concernant. Nous nous intéressons au label rouge. Ce champ est un booléen, et la documentation nous dit qu'il est accessible en écriture. Pour les afficher, nous avons justement besoin de trouver toutes les images ayant ce champ.

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

À ce stade, ce code devrait être assez simple à comprendre mais il contient quelques aspects intéressants sur Lua qui méritent d'être soulignés :

  • ipairs est une fonction standard de Lua pour le parcours itératif des indices numériques d'une table. Nous l'utilisons ici parce que darktable.database a des indices non-numériques qui sont des fonctions permettant de manipuler la base de données elle-même (pour ajouter ou supprimer des images, par exemple).

  • Le parcours itératif de la table va retourner à la fois les clés et les valeurs utilisées. En Lua Il est conventionnel d'utiliser une variable nommée « _ » pour stocker des valeurs dont nous ne nous soucions pas.

  • Notez que nous utilisons ici la fonction Lua standard tostring et non la fonction spécifique darktable darktable.debug.dump. La fonction standard retournera un nom pour l'objet alors que la fonction de débogage affichera son contenu. La fonction de débogage serait ici trop verbeuse. Une fois de plus c'est un excellent outil de débogage mais il ne devrait pas être utilisé pour autre chose.

9.1.4. Ajout d'un raccourci simple

Jusqu'à présent, tous nos scripts ont fait des choses pendant le démarrage. Ceci est d'une utilité limitée et ne nous permet pas de réagir aux actions d'un utilisateur. Pour faire des choses plus avancées, nous devons enregistrer une fonction qui sera appelée en réaction à un événement donné. L'événement le plus commun auquel réagir est un raccourci clavier.

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

Démarrez maintenant darktable, aller à preferences => shortcut => lua => A shortcut that print its parameters pour assigner un raccourci et essayez-le. Vous devriez avoir un message sympa imprimé à l'écran.

Regardons le code en détail. Tout d'abord nous définissons une fonction à deux paramètres. Ces paramètres sont des chaînes de caractères. Le premier est le type de l'événement qui est déclenché ( "shortcut" ) et le second est précisément ce raccourci ( "A shortcut that print its parameters" ). La fonction appelle elle-même darktable.print qui affichera le message en surimpression dans la fenêtre principale.

Une fois cette fonction définie, nous l'enregistrons en tant que fonction de rappel de type shortcut. Pour faire cela nous appelons darktable.register_event qui est une fonction générique pour tous les types d'événements. Nous lui indiquons que nous sommes en train d'enregistrer un événement de type shortcut, puis nous donnons la fonction de rappel à appeler et enfin, nous donnons la chaîne de caractères décrivant le raccourci dans la fenêtre des préférences.

Essayons un raccourci un peu plus interactif. Celui-ci examinera les images qui intéressent actuellement l'utilisateur (sélectionnées ou survolées à la souris) et augmentera leur évaluation.

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

À ce stade, la plupart de ce code doit être compréhensible en soi. Juste quelques notes :

  • Au lieu de déclarer une fonction et de la référencer, nous la déclarons directement dans l'appel de darktable.register_event, ce qui est strictement équivalent mais un peu plus compact.

  • image.rating est le champ d'une image qui donne son évaluation ( entre 0 et 5 étoiles, -1 signifie rejetée).

  • darktable.gui.action_images est une table contenant toutes les images appropriées. darktable traitera les images sélectionnées s'il y en a, et l'image située sous la souris si aucune image n'est sélectionnée. Cette fonction permet de suivre facilement la logique de l'interface utilisateur de darktable dans Lua.

Si vous sélectionnez une image et pressez votre raccourci plusieurs fois, il fonctionnera correctement au début, puis, quand l'évaluation aura atteint cinq étoiles, darktable commencera à montrer sur la console l'erreur suivante :



          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

        

C'est la façon de Lua de déclarer les erreurs. Nous avons essayé de fixer à 6 l'évaluation d'une image, mais une évaluation ne peut pas être supérieure à 5. Ce serait trivial d'ajouter un test, prenons une voie plus compliquée et, au lieu de cela, capturons l'erreur.

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 exécutera son premier argument et capturera toute exception levée par lui. S'il n'y a aucune exception il retournera true et tout résultat renvoyé par la fonction ; s'il y a une exception il retournera false et le message d'erreur de l'exception. Nous testons simplement ces résultats et les affichons sur la console ...

9.1.5. Exportation d'images avec Lua

Nous avons appris à utiliser Lua pour adapter darktable à notre propre flux de travail, regardons comment utiliser Lua pour exporter facilement des images. darktable peut facilement exporter des images vers un bon nombre de services en ligne mais il y en a toujours plus. Si vous êtes en mesure de télécharger une image vers un service via la ligne de commande alors vous pouvez utiliser Lua pour intégrer ceci dans l'interface utilisateur de darktable.

Dans l'exemple qui suit nous utiliserons Lua pour exporter via scp. Un nouvel espace de stockage apparaîtra dans l'interface utilisateur de darktable qui exportera les images vers une cible distante via le mécanisme de copie par 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 ajoutera une nouvelle préférence dans le menu préférences de darktable. scp_export et export_path nous permettent d'identifier notre préférence de manière unique. Ces champs sont réutilisés quand nous lisons la valeur de la préférence. Le champ string indique au moteur Lua que la préférence est une chaîne de caractères. Il peut aussi être un entier, un nom de fichier ou l'un quelconque des types détaillés dans le manuel de l'API relatifs à types_lua_pref_type. Nous avons alors une étiquette pour la préférence dans le menu des préférences, l'info-bulle lors du survol de la valeur et la valeur par défaut.

darktable.register_storage est l'appel qui enregistre réellement un nouveau stockage. Le premier argument est un nom pour le stockage, le deuxième est l'étiquette qui sera affichée dans l'interface utilisateur et le dernier est une fonction à appeler à chaque image. Cette fonction a beaucoup de paramètres, mais filename est le seul que nous utilisons dans cet exemple. Il contient le nom d'un fichier temporaire où l'image a été exportée par le moteur de darktable.

Ce code fonctionnera mais il a quelques limitations. Ce n'est après tout qu'un simple exemple :

  • Nous utilisons les préférences pour configurer le chemin cible. Il serait plus agréable d'ajouter un élément à l'interface d'exportation de darktable. Dans la prochaine section nous expliquerons en détail comment faire cela.

  • Nous ne testons pas la valeur de retour de scp. Cette commande peut échouer, en particulier si l'utilisateur n'a pas configuré correctement la préférence.

  • Ce script ne peut pas lire une entrée de l'utilisateur. Le scp distant doit utiliser un mode de copie sans mot de passe. Scp ne peut pas fournir aisément un mot de passe,

  • Il n'y a pas de message affiché quand l'exécution est terminée, seule la barre de progression en bas à gauche indique à l'utilisateur que le travail est terminé.

  • Nous utilisons coroutine.yield pour appeler un programme externe. Le code normal os.execute bloquerait l'exécution d'autres codes Lua.

9.1.6. Construire des éléments de l'interface utilisateur

Notre exemple précédent est un peu limité. En particulier l'utilisation d'une préférence pour le chemin d'exportation n'est pas très agréable. Nous pouvons faire mieux que cela en ajoutant des éléments à l'interface utilisateur dans la boîte de dialogue d'exportation.

Les éléments de l'interface utilisateur sont créés via la fonction darktable_new_widget. Cette fonction prend en paramètre un type de widget et retourne un nouvel objet correspondant à ce widget. Vous pouvez alors définir différents champs de ce widget pour le paramétrer. Vous pourrez alors utiliser cet objet comme paramètre de différentes fonctions qui l'ajouteront à l'interface utilisateur de darktable. L'exemple simple suivant ajoute à la vue table lumineuse une bibliothèque ayant une simple étiquette.

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)
    

Il existe une belle astuce syntaxique pour rendre le code d'un élément de l'interface plus facile à lire et à écrire. Vous pouvez appeler ces objets comme des fonctions avec une table de valeurs clés en tant qu'arguments. Ceci permet à l'exemple suivant de fonctionner. Il créer un widget conteneur avec deux sous-widgets : une étiquette et un champ texte.

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

Maintenant que nous connaissons cela améliorons un peu notre 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. Partage de scripts

Jusqu'à présent notre code Lua est placé dans le fichier luarc. C'est un bon moyen pour développer votre script mais il n'est pas très pratique pour le distribuer. Nous devons faire ceci dans un module Lua approprié. Pour faire ceci nous sauvegardons le code dans un fichier séparé (scp-storage.lua dans notre cas) :

--[[
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 recherchera des scripts dans les répertoires standard (suivant en cela les règles normales de Lua) et dans $CONFIGDIR/lua/?.lua. Ainsi notre script peut être appelé en ajoutant simplement require "scp-storage" dans le fichier luarc. Quelques notes supplémentaires ...

  • La fonction darktable.configuration.check_version testera la compatibilité pour vous. Le ... se transformera en le nom de votre script et {2,0,0} est la version de l'API que vous avez testée avec votre script. Vous pouvez ajouter de multiples versions de l'API si vous mettez à jour votre script pour de multiples versions de darktable.

  • Assurez-vous de déclarer toutes vos fonctions comme local afin de ne pas polluer l'espace général de nommage.

  • Assurez-vous de ne pas laisser dans votre code des affichages de débogage. En particulier darktable.print_error vous permet de laisser des affichages de débogage dans votre code final sans qu'ils perturbent la console.

  • Vous êtes libre de choisir toute licence pour votre script mais ceux qui sont téléchargés sur le site web de darktable doivent être sous licence GPLv2.

Une fois que vous aurez rempli tous les champs, testé votre code, vous pourrez le télécharger sur notre page de scripts ici.

9.1.8. Appel de Lua à partir de DBus

Il est possible d'envoyer une commande Lua à darktable via son interface DBus. La méthode org.darktable.service.Remote.Lua prend un seul paramètre de type chaîne de caractères qui est interprété comme une commande Lua. La commande sera exécutée dans le contexte Lua courant et devra retourner soit nil soit une chaîne de caractères. Le résultat sera passé comme résultat de la méthode DBus.

Si l'appel à Lua provoque une erreur, l'appel de la méthode DBus retournera une erreur org.darktable.Error.LuaError avec le message d'erreur Lua comme message attaché à l'erreur DBus.

9.1.9. Utiliser darktable à partir d'un script Lua

Attention ! Cette fonctionnalité est très expérimentale. Il est connu que plusieurs éléments de la bibliothèque ne fonctionnent pas encore. Des essais minutieux sont fortement recommandés.

L'interface de Lua vous autorise à utiliser darktable à partir de tout script Lua. Ceci chargera darktable comme bibliothèque et vous fournira l'essentiel de l'API Lua (darktable est alors configuré sans interface et donc les fonctions liées à l'interface utilisateur ne seront pas disponibles).

À titre d'exemple, le programme suivant affichera la liste de toutes les images de votre bibliothèque :

#!/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

Notez la troisième ligne qui pointe vers l'emplacement du fichier libdarktable.so.

Notez aussi que l'appel à require retourne une fonction qui peut être appelée seulement une fois et qui vous permet de définir les paramètres de la ligne de commande de darktable. Le paramètre :memory: de --library est utile ici si vous ne souhaitez pas travailler sur votre bibliothèque personnelle.