GIMP Script-Fu Первый Дан. Расширения к Script-fu

41edc6e5e5af7a386b45e17da7357f95.jpg

Если сравнить расширяемое приложение с коробочкой, то плагины, это полезные вещи, которыми можно наполнить эту коробочку, придающие новое содержание нашему приложению. В этом ряду стоит и такая вещь как Script-fu. Но что если я скажу, что Script-fu так же может быть такой же «коробочкой» и может иметь расширения, давайте разбираться. Но сразу скажу, счастливы будут не все.

Плагины к tinyscheme.

Изначальный интерпретатор на базе которого создавался Script-fu позволяет загружать специальным образом написанные для него плагины. Самих плагинов к тинисхеме написано немного. Да что там немного, раз два и обчёлся. Поэтому я рассмотрю плагин поставляемый с гимпом, tsx, он так и расшифровывается тинисхема экстеншен, т.е расширение.

Загрузка плагина в Script-fu.

Предположим вы разместили плагин к тинисхеме в домашней директории »~/work/scheme/tiny/tsx/». Попробуем дать команду в консоли:

(getenv "HOME") ;;...../work/scheme/tiny 
(define p-ext (string-append (getenv "HOME") "/work/scheme/tiny/")) 
(define p-ext-tsx (string-append p-ext "tsx/tsx"))

(load-extension p-ext-tsx) ;;Error: eval: unbound variable: load-extension

К моему ВЕЛИКОМУ сожалению, дорогие друзья, в стандартных сборках GIMP, из тинисхемы, на базе которой сделан script-fu, удален модуль dynload.c. Его просто нет даже в архивах сборок исходных кодов дистрибутивов GIMP. А именно он отвечает за загрузку расширений в тинисхеме. По какой причине это сделано я не знаю, но зато я знаю как этот модуль добавить в ваш GIMP.

Но я расскажу Вам, как проделал этот процесс на Линуксе, в принципе это можно сделать и на Виндовсе, но это не моя родная система, и у меня там нет никаких рабочих инструментов (MinGW есть обрезанный, обычную tinyscheme и плагины к ней я могу скомпилировать, но уже целый GIMP вряд ли).

Добавление возможности загружать плагины в Script-fu под Linux

Итак, нам надо пересобрать Script-fu, чтобы он поддерживал загрузку расширений. Первым делом вам понадобиться архив исходников вашей установленной сборки GIMP. Её можно скачать например здесь: https://download.gimp.org/gimp/v2.10/ . Качаем к себе и распаковываем. Запускаем ./configure. Эта процедура сгенерирует Makefile, в каждой поддиректории проекта, заодно проверив всё ли есть для сборки проекта GIMP. Нам ВЕСЬ гимп собирать не надо (но желательно, иначе сборка script-fu превратиться «в увлекательное занятие»!

Далее мы идём в каталог: «gimp-2.10.30/plug-ins/script-fu» ищем строчку »-DUSE_INTERFACE=1» в makefile и под ней добавляем две строчки:

   ```
   -DSUN_DL=1      \
   -DUSE_DL=1      \
   ```

так что бы ниже ещё была строчка:

   ```
   -DUSE_STRLWR=0
   ```

В общем это параметры компиляции плагина script-fu (SUN_DL в данной директории не обзяателен).
Переходим в директорию tinyscheme ищем ту же строчку в файле makefile и опять добавляем:

   ```
 -DSUN_DL=1       \
 -DUSE_DL=1       \
   ```

Для систем виндовс надо вместо SUN_DL писать _WIN32.

Далее идем на гитхаб https://github.com/GNOME/gimp/tree/master/plug-ins/script-fu/libscriptfu/tinyscheme — зеркало исходников GIMP, и качаем от туда два файла dynload.c и dynload.h и размещаем их в директории tinyscheme. В makefile в той же директории надо провести ещё одну правку. Ищем строку:

 ```
  m_libtinyscheme_a_OBJECTS = scheme.$(OBJEXT)
 ```
  и добавляем компиляцию dynload

 ```
  m_libtinyscheme_a_OBJECTS = scheme.$(OBJEXT) dynload.$(OBJEXT)`
  ```

Опс. и чуть было не забыл, надо добавить линковку динамического загрузчика библиотек dl в makefile формирующего script-fu

LDADD = \
        -ldl            \
        $(libgimpui)            \
		....

Всё! Дальше идем в директорию выше и собираем плагин скрипт-фу с помощью команды make.

К сожалению для сборки этого небольшого плагина, требуются много сгенерированных и скомпилированных файлов из дистрибутива gimp, типа libgimpui-2.0.la, libgimpmodule-2.0.la и т.п. Их можно сгенерировать вручную прыгая из директории в директорию и давай команду make с нужным файлом, но я советую, всё таки собрать предварительно весь гимп командой make из верхней директории дистрибутива, это долго, но проще, а уже после этого перебраться в директорию script-fu и там дать команду make.

Сам скомпилированный плагин находиться в директории ./.libs. Проверим на всякий случай, есть ли в собранном плагине строчка: load-extension, если нет, то что то у вас пошло не так.

Копируем этот плагин в место, где лежал оригинальный плагин, у меня это директория /usr/lib/gimp/2.0/plug-ins/script-fu, предварительно сохранив оригинальную версию плагина, ОБЯЗАТЕЛЬНО в другой директории (gimp может загрузить и старую переименованную версию плагина)!

Теперь можем грузить гимп, и плагин script-fu, если загрузилось поздравляю всё отлично.
Даже можно дать комманду в консоли скрип-фу:

load-extension
;;#

Мы это сделали!

Библиотеки расширения тинисхемы.

Собственно библиотек расширения тинисхемы, хм. раз два и обчёлся, о чём я уже говорил, вот что я нашёл:
https://github.com/mherasg/tsx — это и есть тинисхема экстеншен.
https://github.com/ignorabimus/tinyscheme-ffi — не пробовал, но по описанию эта библиотека позволяет загрузить любую динамическу библиотеку из тинисхемы, а не только её родное расширение и работать с функциями этой библиотеки как с обычными функциями тинисхемы.
https://sourceforge.net/projects/tinyscheme/files/ — тут лежит помимо самой тинисхемы, библиотека работы с регулярными выражениями из тинисхемы.
http://www.geonius.com/software/tsion/ — работа с сетью, директориями и пр.

Не думаю, что кому-то захочется строить сетевой веб сервер в консоли гимпа, а в принцпе чем чёрт не шутит?))) Но данные библиотеки можно использовать для изучения принципов построения расширений тинисхемы.

Итак если у вас есть уже скомпилированная версия расширений тинисхемы, и вы попробуете её загрузить из консоли script-fu с помощью комманды load-extension, ваша попытка неминуемо окончиться крахом script-fu. Почему это происходит? Дело в том что разработчики scrip-fu существенно изменили тинисхему используемую с гимпом, и все расширения надо компилировать с учётом новых заголовочных файлов поставляемых с исходниками gimp.

Компилируем расширение тинисхемы:

Берём проект tsx и распаковываем его.
Правим makefile

#SCHEME_H_DIR=..
SCHEME_H_DIR=<ВАШ ПУТЬ ДО ИСХОДНИКОВ ГИМП>/gimp-2.10.30/plug-ins/script-fu/tinyscheme
GLIB_H=pkg-config --cflags glib-2.0
GLIB_L=pkg-config --libs glib-2.0

CC=gcc
CFLAGS=-DUSE_DL=1 -I $(SCHEME_H_DIR) $(GLIB_H)

даём команду сборки:

make

При удачном завершении сборки мы получим файл: tsx.so который можно будет загружать в качестве расширения тинисхемы:

(define p-ext (string-append (getenv "HOME") "/work/scheme/tiny/gimp/"))
(define p-ext-tsx (string-append p-ext "tsx/tsx"))
(load-extension p-ext-tsx)

Теперь нам доступна интересная команда для работы:

(system "pwd")
(system (string-append "ls " (getenv "HOME") "/work/gimp"))

Вот только результаты её работы вы сможете увидеть, если запустили гимп из какой нибудь консоли, и будут они именно в ней! Команда не вернет никаких данных, кроме кода возврата команды, ну это уже вопросы к разработчику данного расширения, почему оно так работает. Но то, что теперь из тинисхемы мы можем выполнять системные команды уже хорошо.

Пишем собственное расширение тинисхемы.

Для написания расширения тинисхемы нам понадобиться небольшое знание Си. В качастве примеров я выбрал несколько функций битовых операций, отсутствующих в оригинальной тинисхеме, операцию получения случайных чисел и альтернативу функции system из tsx, позволяющую получать консольный вывод комманды.

Расширение размещаем в отдельной директории и загружаем с помощью комманд:

(define p-ext (string-append (getenv "HOME") "/work/scheme/tiny/gimp/"))
(define p-ext-my (string-append p-ext "my-ext/myext"))
(load-extension p-ext-my)

Начнем с самой простой функции, получения случайного числа:

pointer foreign_rand(scheme * sc, pointer args)
{
  pointer ret;
  unsigned long    rez;

  rez = rand();
  ret = sc->vptr->mk_integer(sc,rez);
  return ret;
}

Нам ничего не надо передавать в функцию, а просто получить случайное число с помощью функции rand и сформировать значение в схеме на основе этого числа, это можно сделать с помощью вызова функции sc->vptr->mk_integer(sc,rez).

Чтобы наша функция была доступна в тинисхеме, надо написать функцию инициализации, которую выполняет модуль dynload при загрузке динамической библиотеки расширения. В ней выполняются функции определения для схемы, связывающие код функции с именем функции в схеме. Там же инициализируем генератор случайных чисел: srand(time(NULL)); В эту же функцию будем добавлять определения для схемы и других функций расширения.

void init_myext (scheme * sc)
{
  printf("in run init_myext"); //просто печать для отладки
  ......
  srand(time(NULL));
  sc->vptr->scheme_define(sc,sc->global_env,
                               sc->vptr->mk_symbol(sc,"rand"),
                               sc->vptr->mk_foreign_func(sc, foreign_rand));
  .....
  printf("exit from init_myext");
}

Теперь разберёмся с передачей значений из схемы в функцию си, на примере написания битовой операции отритцания. Для выполнения этой операции нам надо передать одно целое значение в функцию си. Но поэксперементировав с этой функцией я решил, что лучше передавать еще одно значение, битовую маску нашего интереса, означающую сколько (а по факту какие) значащих битов мы хотим получить из функции.

pointer foreign_bitwise_not(scheme * sc, pointer args)
{
  pointer first_arg;
  unsigned long    var;
  pointer ret;
  pointer interest_arg;
  unsigned long    interest;
  unsigned long    rez;

  if(args == sc->NIL)
  {
    return sc->F;
  }

  first_arg = sc->vptr->pair_car(args);//получаем первое значение

  if(!sc->vptr->is_integer(first_arg))
  {
    return sc->F;
  }
  var = sc->vptr->ivalue(first_arg); //получаем первое значение

  args = sc->vptr->pair_cdr(args);   //получаем второе значение
  interest_arg = sc->vptr->pair_car(args);
  if(!sc->vptr->is_number(interest_arg)) {
    return sc->F;
  }
  interest = sc->vptr->ivalue(interest_arg); //получаем второе значение

  rez = (~var) & interest;      //выполняем операцию

  ret = sc->vptr->mk_integer(sc,rez);  //возвращаем значение
  return ret;
}

Аргументы передаваемые через args, представляют собой список из тинисхемы, нам надо просто его разобрать и преобразовать значения схемы в значения си.
Далее также надо зарегистрировать нашу функцию и сделать её доступной для схемы в функции init_myext

sc->vptr->scheme_define(sc,sc->global_env,
                           sc->vptr->mk_symbol(sc,"bitwise-not"),
                           sc->vptr->mk_foreign_func(sc, foreign_bitwise_not));

Давайте попробуем пример:

(bitwise-not #x0 #xFF)
;;255
(bitwise-not #x01 #xFF)
;;254
(bitwise-not #x01 #xFFFF)
;;65534
(number->string 5 2)
;;"101"
(number->string (bitwise-not  5 #xF) 2)
;;"1010"

Мы разобрали унарную функцию побитового отритцания. А следующие функции будут бинарные (имеющие два операнда) и ещё один дополнительный аргумент маску интересующих нас битов. Приведу полный пример операции побитового или.

pointer foreign_bitwise_or(scheme * sc, pointer args)
{
  pointer first_arg;   pointer second_arg;   unsigned long    var1, var2;
  pointer ret;
  pointer interest_arg;   unsigned long    interest;
  unsigned long    rez;

  if(args == sc->NIL)
  {
    return sc->F;
  }

  first_arg = sc->vptr->pair_car(args); //первый аргумент
  if(!sc->vptr->is_number(first_arg))
  {
    return sc->F;
  }
  var1 = sc->vptr->ivalue(first_arg);   //первый аргумент

  args = sc->vptr->pair_cdr(args);      //второй аргумент
  second_arg = sc->vptr->pair_car(args);
  if(!sc->vptr->is_number(second_arg)) {
    return sc->F;
  }
  var2 = sc->vptr->ivalue(second_arg);   //второй аргумент

  args = sc->vptr->pair_cdr(args);       //третий аргумент
  interest_arg = sc->vptr->pair_car(args);
  if(!sc->vptr->is_number(interest_arg)) {
    return sc->F;
  }
  interest = sc->vptr->ivalue(interest_arg);//третий аргумент


  rez = (var1 | var2) & interest;           //операция побитового или и наложение маски

  ret = sc->vptr->mk_integer(sc,rez);     //возврат значения
  return ret;
}

Операция побитового и отличается от или, только выполняемой над аргументами операцией.

rez = (var1 & var2) & interest;

Операция побитового исключающего или, аналогична:

rez = (var1 ^ var2) & interest;

Также надо прописать эти функции в операцию иницализацию расширения:

sc->vptr->scheme_define(sc,sc->global_env,
                             sc->vptr->mk_symbol(sc,"bitwise-or"),
                             sc->vptr->mk_foreign_func(sc, foreign_bitwise_or));
sc->vptr->scheme_define(sc,sc->global_env,
                             sc->vptr->mk_symbol(sc,"bitwise-and"),
                             sc->vptr->mk_foreign_func(sc, foreign_bitwise_and));
sc->vptr->scheme_define(sc,sc->global_env,
                             sc->vptr->mk_symbol(sc,"bitwise-xor"),
                             sc->vptr->mk_foreign_func(sc, foreign_bitwise_xor));

Проверим работу операций:

 (number->string (bitwise-or #b010101 
                             #b000111
			                 #b111111) 2)
;;"10111" 
 (number->string (bitwise-and #b010101 
                              #b000111
			                  #b111111) 2)
;;"101"
 (number->string (bitwise-xor #b010101 
                              #b000111
			                  #b111111) 2)
;;"10010"

Ну, а теперь осталось написать аналог функции system из tsx

pointer foreign_cmd_run(scheme * sc, pointer args)
{
  pointer first_arg;
  char*   command;
  pointer ret;
  FILE*   fp;
  char    arr[LINESIZE];
  
  if(args == sc->NIL)
    return sc->F;

  first_arg = sc->vptr->pair_car(args); //принимаем первый аргумент
  if(!sc->vptr->is_string(first_arg))
    return sc->F;
  
  command = sc->vptr->string_value(first_arg);
  if(0 == command)
    return sc->F;                      //принимаем первый аргумент

  fp = popen(command,"r");             //запускаем на исполнение комманду, создавая канал.
  //чтение простое, не обрабатывает слишком длинные строки, надо бы проверять сколько прочитали
  //и если строка слишком длинная складывать её с остатком.
  ret = sc->NIL;
  while (fgets(arr, LINESIZE, fp) != NULL) {
    printf("read: %s", arr);
    ret = sc->vptr->cons(sc, sc->vptr->mk_string(sc, arr), ret);
  }
  pclose(fp);
  // возвращаем перевёрнутый список вывода, вертеть обратно надо уже в схеме
  return ret;
}

И эту функцию также надо зарегистрировать с схеме в функции инициализации расширения.

sc->vptr->scheme_define(sc,sc->global_env,
                             sc->vptr->mk_symbol(sc,"cmd-run"),
                             sc->vptr->mk_foreign_func(sc, foreign_cmd_run));

Пробуем:

(cmd-run "pwd") ;;("/usr/bin\n")#

(cmd-run (string-append "ls " (getenv "HOME")))
;;("work\n" "video\n" "tmp\n" "quicklisp\n" "inst\n"
;; "game\n" "doc\n" "distr\n" "bin\n" "basedraw01.png\n" "asdf\n"
;; "Videos\n" "Templates\n" "Public\n" "Pictures\n" "Photo\n" "Music\n"
;; "Downloads\n" "Documents\n" "Desktop\n")

(reverse (cmd-run (string-append "/sbin/ifconfig" " -a")))
;;("eth1: flags=4163  mtu 1500\n" 
;;"        inet ...
;;...
;;"        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0\n" 
;;"\n")

Если мы разместим макросы которые мы написали раньше, в файле util.scm.

Продемонстрирую сеанс работы со скрипт фу:

;;определяем пути и загружаем библиотеку и расширение my-ext
(define path-home (getenv "HOME"))
(define path-lib (string-append path-home "/work/gimp/lib/"))
(define path-work (string-append path-home "/work/gimp/"))
(load (string-append path-lib "util.scm"))
(define p-ext (string-append path-home "/work/scheme/tiny/gimp/"))
(define p-ext-my (string-append p-ext "my-ext/myext"))
(load-extension p-ext-my)

;;определяем функию получения случайного числа на основе функции из my-ext
(define (random i)
  (modulo (rand) i))

;;определяем функцию получения списка случайных чисел заданной длины и огр. m
(define (rand-lst m len)
    (let ((rez '()))
	   (for (i 1 len)
	      (set! rez (cons (random m)
		                  rez)))
	    rez))
		
(rand-lst 21 30)
;;(15 10 18 15 13 0 5 3 13 8 14 2 12 3 7 3 8 1 19 10 12 13 17 18 3 9 16 17 18 13)
(rand-lst 21 30)
;;(7 17 15 18 1 0 4 1 16 3 11 9 10 11 18 9 4 17 3 2 0 13 11 8 19 16 6 12 2 10)

Вот так вот легко и «непринужденно» мы добавили в Script-fu несколько функций, вернее написали расширение при загрузке которого в Script-fu добавляются несколько функций.

Таким образом поддержка загрузки расширений в тинисхеме является мощным средством расширения функциональности script-fu.

© Habrahabr.ru