darktable page lede image
darktable page lede image

Rozdział 9. Skrypty Lua

darktable wyposażony jest w elastyczny interfejs skryptowy dla rozszerzenia swojej funkcjonalności.

9.1. Użycie Lua

Lua może być wykorzystane do określenia akcji, które darktable wykona po zajściu określonego zdarzenia. Można na przykład podczas eksportu pliku wywołać zewnętrzną aplikację w celu przeprowadzenia dodatkowej obróbki obrazu poza darktable.

darktable używa Lua, będącego niezależnym projektem, założonym w 1993 r. i dostarczającym potężny, szybki, lekki i możliwy do osadzenia język skryptowy. Lua jest szeroko wykorzystywany w wielu otwartoźródłowych aplikacjach, programach komercyjnych oraz przy programowaniu gier.

darktable korzysta z Lua w wersji 5.3. Opis zasad i składni Lua wykracza poza zakres tego podręcznika. Dokładny wstęp zawiera instrukcja referencyjna Lua.

9.1.1. Podstawowe zasady

Podczas startu darktable wykonywane są dwa skrypty Lua:

  • skrypt luarc in $DARKTABLE/share/darktable

  • skrypt luarc z katalogu konfiguracyjnym użytkownika

Zmienna $DARKTABLE oznacza katalog instalacyjny darktable.

Ten jeden raz darktable samodzielnie uruchamia skrypty Lua. Skrypt potrafi rejestrować wywołanie zwrotne, żeby wykonać akcje na różnych zdarzeniach darktable. Mechanizm wywołań zwrotnych jest podstawowym sposobem do wyzwalania akcji Lua.

9.1.2. Prosty skrypt Lua

Rozpocznijmy prostym przykładem, wypisującym trochę kodu na konsolę. Stwórz plik luarc w katalogu konfiguracyjnym użytkownika (zazwyczaj ~/.config/darktable/) i dodaj do niego linię:

print("Hello World !")

Uruchom darktable, a na konsoli pojawi się zdanie Witaj, świecie!. Na razie szału nie ma, ale spokojnie, zaraz się rozkręci...

Do tego miejsca w skrypcie nie ma nic charakterystycznego dla darktable. Chcąc wypisać łańcuch, wykorzystaliśmy po prostu standardową funkcję print. Działa, ale stać nas na więcej. Dla dostępu do API potrzebujemy wywołać require i zapisać zwrócony obiekt do zmiennej. Po wykonaniu tego zyskujesz dostęp do API darktable jako składowych zwróconego obiektu. Całość udokumentowana jest w podręczniku API darktable (p. Sekcja 9.2, „Lua API”).

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

Uruchamiasz skrypt… i nic się nie dzieje. Funkcja darktable.print_log działa jak print, ale drukuje komunikaty tylko wtedy, kiedy uruchomiłeś śledzenie lua przełącznikiem -d lua z linii poleceń. Jest to zalecana metoda debugowania w skrypcie lua.

9.1.3. Drukowanie oznakowanych zdjęć

Pierwszy przykład pokazał zupełne podstawy Lua i pozwolił na sprawdzenie, czy wszystko działa. Przyszedł czas na coś bardziej złożonego. Spróbujmy wydrukować listę zdjęć, posiadających czerwoną etykietę. Ale najpierw… ustalmy, czym jest zdjęcie.

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

Uruchomienie kodu wygeneruje wiele komunikatów. Przyjrzymy się im za moment, ale teraz zerknijmy na sam kod.

Umiemy już wywołać darktable. Wywołamy teraz oddzielnie darktable.debug, będące opcjonalną sekcją API, dostarczającą funkcji, pomagających diagnozować błędy skryptów Lua.

darktable.database jest tabelą, dostarczaną przez API, a zawierającą wszystkie zdjęcia w bazie (aktualnie widoczne bądź nie, zduplikowane bądź nie...). Każdy zapis w bazie jest obiektem zdjęcia. Obiekty zdjęć są złożone i pozwalają na manipulację zdjęciami na różne sposoby (udokumentowane w sekcji types_dt_lua_image_t podręcznika API). Do wyświetlenia zdjęć użyjemy darktable.debug.dump, co jest funkcją, biorącą dowolny argument i zwracającą jego zawartość. Ponieważ zdjęcia są skomplikowanymi obiektami, odwołującymi się do innych obiektów, zrzut ich zawartości może być potężny. Poniżej okrojony przykład takiego wyjścia.



          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

        

Jako można zobaczyć, zdjęcie posiada dużą liczbę pól, dostarczających wielu informacji. My jesteśmy zainteresowani czerwoną etykietą. To pole ma wartość zerojedynkową, a dokumentacja mówi nam, że może być zapisywane. Potrzebujemy po prostu znaleźć wszystkie zdjęcia z tym polem i wylistować je.

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

Kod do tego miejsca powinien być prosty do zrozumienia, ale pokazuje kilka ciekawych aspektów języka Lua, wartych podkreślenia:

  • ipairs jest standardową funkcją Lua, iterującą wszystkie indeksy numeryczne w tabeli. Użyliśmy tego, ponieważ darktable.database posiada indeksy nienumeryczne, które są funkcjami do manipulacji samą bazą (jak na przykład dodawanie albo usuwanie zdjęć).

  • Iteracja przez tablicę zwróci zarówno klucz, jak i użytą wartość. Konwencją języka jest użycie zmiennej o nazwie _ do przechowania wartości, na których nam nie zależy.

  • Zauważ, że korzystamy ze standardowej funkcji Lua tostring, a nie z funkcji darktable darktable.debug.dump. Standardowa funkcja zwróci nazwę obiektu, a funkcja debug – wypisze jego zawartość. Dlatego tez funkcja debug jest jak na nasze potrzeby zbyt gadatliwa. Jest ona naprawdę dobra do analizy kodu, ale nie powinna być używana nigdzie indziej.

9.1.4. Dodawanie prostego skrótu klawiszowego

Nasze skrypty jak dotychczas robiły coś przy starcie. Niewielki z tego pożytek i nie pozwala to na reakcję na faktyczne działania użytkownika. Żeby zrobić coś więcej, potrzebujemy funkcji, wywoływanej w reakcji na określone zdarzenie. Najpopularniejszym zdarzeniem jest reakcja na wciśnięcie skrótu klawiszowego.

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

A teraz uruchom darktable, idź do preferencje => skroty => lua => Skrót wypisujacy swoje parametry, przypisz skrót klawiszowy i wypróbuj to. Na ekranie powinien pojawić się zgrabny komunikat.

Przyjrzyjmy się bliżej skryptowi. Najpierw definiujemy funkcję z dwoma parametrami; są to łańcuchy znaków. Pierwszy to typ wyzwalanego zdarzenia ("shortcut"), a drugi to jego opis ("Skrót wypisujacy swoje parametry"). Funkcja wywołuje darktable.print, które wypisuje komunikat w formie nakładki na głównym ekranie.

Po zdefiniowaniu funkcji rejestrujemy ją jako wywołanie zwrotne skrótu. W tym celu wywołujemy darktable.register_event, będące ogólną funkcją dla wszystkich typów zdarzeń. Mówimy jej, że rejestrujemy zdarzenie skrótu, następnie przekazujemy wywołanie zwrotne, a na końcu – łańcuch do opisania skrótu w oknie preferencji.

Stwórzmy teraz skrót nieco bardziej interaktywny. Ten sprawdzi, którymi zdjęciami (poprzez zaznaczenie lub najechanie kursorem) użytkownik jest zainteresowany i zwiększy ich rating.

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

Do tego miejsca kod powinien objaśniać się sam. Parę drobnych uwag:

  • Zamiast deklarować funkcję i odwołać się do niej, zadeklarujemy ją bezpośrednio w wywołaniu darktable.register_event, co da ten sam efekt, a ma zwięźlejszą formę.

  • image.rating to własność każdego zdjęcia, zwracająca jego ocenę (pomiędzy 0 a 5 gwiazdek, -1 oznacza odrzucone).

  • darktable.gui.action_images jest tabelą, zawierającą wszystkie interesujące nas zdjęcia. darktable zadziała na zaznaczonych obrazach, jeśli którykolwiek jest zaznaczony, lub na obrazie pod kursorem, jeśli nie zaznaczono żadnego. Funkcja pomaga odwzorować w Lua logikę interfejsu darktable.

Jeśli zaznaczysz zdjęcie i wciśniesz skrót kilka razy, zadziała on poprawnie za pierwszym razem, ale gdy osiągniesz pięć gwiazdek, darktable pokaże na konsoli następujący błąd:



          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

        

W ten sposób Lua raportuje błędy. Próbowaliśmy ustawić dla zdjęcia ocenę 6, podczas gdy program dopuszcza maksymalnie 5. Sprawdzenie tego byłoby trywialne, ale nie pójdziemy na łatwiznę i zamiast tego obsłużymy wyjątek.

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 uruchomi swój pierwszy argument i obsłuży każdy wyjatek, rzucony przez niego. Jeśli nie będzie wyjątku, zwróci true oraz każdą wartość, zwróconą przez funkcję; jeśli będzie, zwróci false i komunikat o błędzie wyjątku. Przetestujemy te rezultaty i wypiszemy je na konsoli…

9.1.5. Eksport obrazów z Lua

Nauczyliśmy się używać lua do pomocy darktable w naszym schemacie pracy, zobaczymy teraz, jak użyć go do eksportu zdjęć. darktable potrafi eksportować obrazy do kilku serwisów online, ale zawsze znajdą się jakieś jeszcze nieobsługiwane. Jeśli potrafisz przesłać zdjęcie do serwisu z linii poleceń, możesz użyć Lua w celu integracji tego z interfejsem darktable.

W następnym przykładzie użyjemy Lua do eksportu poprzez scp. W interfejsie pojawi się nowy magazyn, z którym zdjęcia zostaną wyeksportowane na zdalny serwer z wykorzystaniem mechanizmu kopiowania 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 doda nową pozycję w menu preferencji darktable. scp_export i export_path pozwalają na jednoznaczne ustawienie pożądanej wartości. Te pola zostaną użyte powtórnie podczas odczytu preferencji. string mówi silnikowi lua, że pozycja preferencji jest łańcuchem znakowym. Może to również być liczba całkowita, nazwa pliku lub dowolna inna dana, opisana w API w sekcji types_lua_pref_type. Dalej określamy etykietę objaśniającą w menu, podpowiedź wyświetlaną po najechaniu na pole kursorem myszy oraz wartość domyślną.

darktable.register_storage jest wywołaniem, rejestrującym nowy magazyn. Pierwszy argument to nazwa magazynu, drugi to jego etykieta, wyświetlana w interfejsie, a ostatni to funkcja do wywołania na każdym zdjęciu. Funkcja ma wiele parametrów, ale w tym przykładzie potrzebujemy jedynie nazwy pliku. Jest to nazwa tymczasowego pliku, do którego silnik darktable wyeksportował zdjęcie.

Kod zadziała, ale ma szereg ograniczeń. To prosty przykład, ale jeszcze:

  • Korzystamy z preferencji, żeby skonfigurować docelową ścieżkę. Byłoby dobrze dodać element do interfejsu eksportu w darktable. Pokażemy to w następnej sekcji.

  • Nie sprawdzamy zwracanej wartości scp. Polecenie może zakończyć się błędem, szczególnie jeśli użytkownik nieprawidłowo ustawił preferencje.

  • Ten skrypt nie potrafi odczytać wejścia od użytkownika. Zdalne scp musi skorzystać z kopiowania bezhasłowego. Do scp nie można w łatwy sposób przekazać hasła, zatem zostawimy to tak, jak jest.

  • Po zakończeniu przykładu nie wyświetla się żadna wiadomość, o końcu zadania informuje jedynie pasek postępu w lewym dolnym rogu.

  • Używamy darktable.control.execute do wywołania aplikacji zewnętrznej. Standardowe os.execute zablokowałoby wykonanie innego kodu lua.

9.1.6. Tworzenie elementów interfejsu użytkownika

Nasz poprzedni przykład był nieco ograniczony – szczególnie skorzystanie z zapisu preferencji dla ścieżki eksportu nie było ciekawe. Ulepszymy to, dodając elementy do interfejsu użytkownika w dialogu eksportu.

Elementy interfejsu tworzone są funkcją darktable_new_widget. Bierze ona typ kontrolki jako parametr i zwraca nowy obiekt, będący tą kontrolką. Możesz na niej zdefiniować różne pola, żeby następnie ustawić ich parametry. Następnie można użyć tego obiektu jako parametru dla różnych funkcji, które dodadzą go do interfejsu darktable. Poniższy prosty przykład dodaje bibliotekę z prostą etykietą w widoku stołu podświetlanego:

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)    

Istnieje sprytny sposób na uproszczenie kodu interfejsu. Możemy wywołać te obiekty jako funkcje z tablicą wartości jako argumentem. Dzięki tej sztuczce działa następny przykład. Tworzy on kontener z dwiema kontrolkami, etykietką i polem tekstowym.

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

Skoro to już wiemy, ulepszymy trochę nasz skrypt.

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. Współdzielenie skryptów

Do tej pory kod Lua umieszczaliśmy w luarc. To jest dobre do pisania skryptu, ale już niekoniecznie w dystrybucji. Zrobimy to porządnie, w Lua. Najpierw zapiszemy kod w oddzielnym pliku (w naszym przykładzie będzie to scp-storage.lua):

--[[
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,
})

darktable w poszukiwaniu skryptów (spełniających normalne kryteria lua) przeszuka standardowe katalogi oraz $CONFIGDIR/lua/?.lua. Nasz skrypt może więc być wywołany poprzez dodanie require "scp-storage" do luarc. Kilka uwag…

  • Funkcja darktable.configuration.check_version sprawdzi kompatybilność. ... zostanie zamienione nazwą skryptu, a {2,0,0} to wersja API, z którą go testowałeś. Możesz dodać wiele wersji API, jeśli aktualizujesz skrypt pod kątem różnych wersji darktable.

  • Upewnij się, że wszystkie funkcje deklarujesz jako lokalne, żeby nie zaśmiecić ogólnej przestrzeni nazw.

  • Upewnij się, że nie zostawisz w kodzie komunikatów debuggera. Szczególnie darktable.print_log pozwoli na pozostawienie komunikatów diagnostycznych w finalnej wersji kodu bez angażowania konsoli.

  • Dla swojego skrytpu możesz wybrać dowolną licencję, ale te, które chcesz umieścić na witrynie darktable, muszą być na GPLx2.

Jeśli wypełniłeś już wszystkie pola i sprawdziłeś kod, możesz przesłać go na naszą stronę skryptów tutaj.

9.1.8. Wywołanie Lua z DBus

Możliwe jest wysłanie komendy Lua poprzez jego interfejs DBus. Metoda org.darktable.service.Remote.Lua pobiera pojedynczy łańcuch tekstowy, interpretowany jako komenda Lua. Polecenie zostanie wykonane w bieżącym kontekście Lua i powinno zwrócić albo nil, albo łańcuch. Zwrócona wartość będzie przekazana powrotnie jako wynik metody DBus.

Jeśli wywołanie Lua zwróci błąd, to samo (błąd org.darktable.Error.LuaError) zwróci również wywołanie metody DBus. Oryginalny komunikat o błędzie zostanie w takim przypadku dołączony do odpowiedniego komunikatu błędu DBus.

9.1.9. Użycie darktable ze skryptu Lua

Ostrzeżenie: Ta opcja jest bardzo eksperymentalna. Wiemy, że kilka elementów nie chce jeszcze pracować w trybie biblioteki. Zalecamy ostrożne testy.

Interfejs Lua pozwala na użycie darktable z dowolnego skryptu. Załaduje on darktable jako bibliotekę i zapewni dostęp do większości API Lua (darktable zostanie uruchomiony w trybie "headless", więc funkcje dostarczane z poziomu interfejsu graficznego nie będą dostępne).

Poniższy program wydrukuje listę wszystkich obrazów w pamięci:

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

Zwróć uwagę na trzecią linię, wskazującą lokalizację pliku libdarktable.so.

Zauważ również, że wywołanie require zwraca funkcję, która może być użyta tylko raz i pozwala na ustawienie parametru linii poleceń darktable. Ustawienie parametru :memory: na --library przydaje się, gdy nie chcesz pracować na swojej osobistej bibliotece.