GIMP Script-fu: быстрое изучение и написание простых скриптов на Scheme (+ пакетная обработка бесплатно)

  • Tutorial

Вступление


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

Содержание:
  1. Что нам понадобится?
  2. Коротко о синтаксисе
  3. Переменные
  4. Функции
  5. Списки
  6. Регистрация скрипта в GIMP
  7. Написание кода
  8. Заключение

Что нам понадобится?


Английский интерфейс: для этого достаточно создать переменную окружения «LANG» со значением «en». Зачем это нужно? Во-первых, так будет проще искать соответствие процедур объектам интерфейса. Во-вторых, мне не придется приводить команды на двух языках. В-третьих, на английском языке в интернете больше всего информации.
Консоль Script-fu: Filters → Script-fu → Console. Здесь мы сможем тестировать небольшие кусочки кода, — то, что нужно при освоении языка.
Обозреватель процедур: Help → Procedure Browser. Здесь можно без особого труда найти функцию, выполняющую требуемое действие и прочитать ее полное описание (всё хорошо задокументировано).
Редактор кода с подсветкой и/или подсчётом парных скобок. Оставлю на ваш вкус. Мне хватило Notepad++. Но учтите, скобок будет много!

В нескольких последующих секциях содержатся выдержки из первых четырех страниц документации Script-fu и немного отсебятины. Настоятельно рекомендуется попробовать запустить приведенные ниже примеры в консоли.

Коротко о синтаксисе

  • Все выражения в Scheme должны быть окружены скобками.
  • Имя функции всегда идёт первым в скобках, а затем идут её параметры.
  • Математические операторы также являются функциями.
Самое время привести пример:
(* (+ 1 2) (sqrt (- 13 4)) 10)

Последним будет посчитан результат умножения. Как видно, функции умножения передаётся три аргумента: результат сложения, результат извлечения корня из разности и число. Обратите внимание на количество скобок: они здесь везде обязательны. Это может мешать, но зато всегда понятно, что́ за чем вычисляется.
  • Функция и каждый из аргументов должны быть отделены друг от друга пробелами.
Пример: «(+ 1 2)» — корректный код, «(+1 2)» — не является таковым.
  • Всё, что идет за символом «;», является комментарием и игнорируется.

Переменные


Переменные в Scheme определяются с помощью конструкции let*. Общий вид:
(let*
 (
  (переменная значение)
  ...
  (переменная значение)
 )
 (выражение)
 ...
 (выражение)
)

Если сравнивать с императивными языками, то это что-то вроде объявления локальных переменных. Иными словами, после скобки, закрывающей конструкцию let*, переменные перестают существовать.
Пример:
(let*
 (
  (a 1)
  (b (+ a 2))
 )
 (+ a b)
)

Ещё пример:
(let*
 ( (x 9) )
 (sqrt x)
)

Обратите внимание, что даже в том случае, когда мы определяем только одну переменную, внешние скобки для списка переменных не опускаются!

Новое значение переменной можно присвоить с помощью конструкции set!:
(set! переменная значение)

Пример:
(let*
 ( (a 42) (b 21) (x 0) )
 (set! x (/ a b))
)

Функции


Свои функции можно определить с помощью конструкции define:
(define (имя_функции аргументы) код_функции)

Значением функции будет результат выполнения последней команды в коде функции.
Реализуем функцию вычисления разницы модулей (). Это можно сделать с помощью abs, но мы сделаем чуть сложнее:
(define (difference x y)
 (if (< x 0) (set! x (- x)))
 (if (< y 0) (set! y (- y)))
 (if (> x y) (- x y) (- y x))
)

Здесь мы использовали конструкцию if, которая проверяет истинность первого своего аргумента и в зависимости от этого выполняет либо второй, либо третий аргумент (причём последний, как можно видеть, необязателен).
Обратите внимание, что функция может обращаться со своими аргументами как с переменными, но изменяет она лишь их копии. В этом можно убедиться следующим образом:
(let* ((a 3) (b -4)) (list (difference a b) a b))

(Функция list здесь используется для вывода нескольких результатов — значения функции, переменной a и переменной b, — а подробнее о списках мы поговорим ниже). Запустите в консоли и проверьте, что значения переменных не изменились.

Списки


Чтобы определить список, достаточно написать (никаких запятых):
'(0 1 1 2 3 5 8 13)

Пустой список можно задать как через «'()», так и через «()». Списки могут содержать как атомарные значения, так и другие списки:
(let*
 (
  (x
   '("GIMP" (1 2 3) ("is" ("great" () ) ) )
  )
 )
 x
)

Поскольку один апостроф мы уже написали, внутренние списки предварять им необязательно.
Чтобы добавить к началу списка еще один элемент, нужно воспользоваться функцией конкатенации cons:
(cons 1 '(2 3 4) )

Она одинаково хорошо работает и с пустыми списками («(cons 1 () )» даст список из одного элемента).
Для создания списка, содержащего значения ранее объявленных переменных, потребуется функция list:
(let* ( (a 1) (b 2) (c 3) )
 (list a b c 4 5)
)

Чтобы понять разницу с определением списка через апостроф, замените «(list a b c 4 5)» на «'(a b c 4 5)» и сравните вывод.
Это всё хорошо, а как же получить содержимое списка? Для этого есть две функции. Первая, car, возвращает голову списка, то есть первый элемент. Вторая, cdr, возвращает хвост списка, то есть список, содержащий все элементы кроме первого. Обе функции предполагают, что список непуст. Примеры:
(car '(1 2 3 4) )
(cdr '(1 2 3 4) )
(car '(1) )
(cdr '(1) )

Вместо последовательного вызова car и cdr бывает полезно воспользоваться функциями типа caadr, cddr и т.п. Например, чтобы получить второй элемент списка, напишем следующее:
(cadr '("first" "second") )
что эквивалентно
(car (cdr '("first" "second") ) )

В следующем примере попробуйте добраться до элемента 3, используя только два вызова таких функций:
(let* ( (
 x  '( (1 2 (3 4 5) 6)  7  8  (9 10) )
 ) )
 ; здесь ваш код
)

Если у вас получилось, значит, вы уже почти готовы написать свой первый скрипт.

Регистрация скрипта в GIMP


Прежде чем садиться писать код, обеспечим себе для этого удобные условия.

Для скриптов пользователя GIMP создает в домашней директории папку .gimp-2.6/scripts. Чтобы скрипт подцепился, достаточно поместить в неё scm-файл и в меню GIMP выбрать Filters → Script-fu → Refresh Scripts (это если GIMP уже запущен, а иначе он сам всё загрузит при запуске).

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

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

Пусть мы хотим написать функцию, улучшающую качество текста на снимке с неравномерной освещённостью (на самом деле, я её уже написал, но это не мешает нам сделать это ещё раз). Вот её определение:
(define (script-fu-readability inImage inLayer inRadius inHigh-input))

Знаю-знаю, здесь только объявление функции, и она ничего не делает. Полезный код будет чуть позже. Сейчас нам и этого вполне достаточно. Регистрация же происходит вот так:
(script-fu-register
 "script-fu-readability"
 "Readability"
 "Improves text readability on the photos. It's needed only when there is a non-uniform illumination"
 "Dragonizer"
 "Copyleft, use it at your own sweet will"
 "January 7, 2011"
 "RGB* GRAY* INDEXED*"
 SF-IMAGE      "The image"     0
 SF-DRAWABLE   "The layer"     0
 SF-ADJUSTMENT "Median blur: radius" '(15 1 20 1 5 0 SF-SLIDER)
 SF-ADJUSTMENT "Levels: intensity of highest input" '(235 0 255 1 10 0 SF-SPINNER)
)
(script-fu-menu-register "script-fu-readability" "<Image>/Filters/User's scripts")

В первую функцию передается следующее. Первый аргумент — имя нашей функции, второй — отображаемое имя, третий — описание, четвертый — автор, пятый — сведения о копирайте, шестой — дата создания. Седьмой — типы поддерживаемых изображений (RGB, RGBA, GRAY, GRAYA, INDEXED, INDEXEDA).

Последующие аргументы являются необязательными. Они (кроме SF-IMAGE и SF-DRAWABLE) позволяют создать в окошке скрипта виджеты, такие как строчки, галки, слайдеры, спиннеры, выбор цвета, шрифта и многое другое, чтобы передать выбор пользователя в функцию. Упомянутый же SF-IMAGE передаст нам ссылку на текущее открытое изображение, а SF-DRAWABLE — на выбранный слой. Я не буду описывать все эти SF-*, их параметры вы можете посмотреть в таблицах здесь (остальное читать не нужно, ибо кратко изложено в этой статье). А ещё советую поглядеть вот эту картинку, чтобы понять, что же вам из этого понадобится (взял отсюда).

Окошко готово, осталось добавить его вызов в меню GIMP, что и делает вторая функция кода выше. Два аргумента: опять же, имя функции и путь к меню. Путь начинается с <Image>, если каких-то веток ранее не существовало, GIMP их добавит.

Ещё пример: если бы мы захотели написать скрипт, создающий изображение с заданными свойствами, мы бы убрали параметры SF-IMAGE и SF-DRAWABLE из первой функции, вместо "RGB* GRAY* INDEXED*" использовали бы пустую строку "" (нам ведь не нужно открытое изображение, мы его создадим), а во второй функции изменили бы путь на что-то вроде "<Image>/File/Create/Something".

Чтобы полюбоваться результатом, сохраним наше творчество в «script-fu-readability.scm» и обновим скрипты. Теперь откроем/создадим какое-нибудь изображение и выберем из меню наш скрипт.

Написание кода


Вот он, вожделенный момент! Но поспешу расстроить: ничего сложного тут нет. Совсем. Функции вы уже писать умеете. А всё, что вам может понадобиться от редактора, легко найти в обозревателе процедур. Нужна какая-то операция со слоями? Ищите по запросу «layer». Инвертировать изображение? Вам нужно что-то, содержащее «invert». И так далее.

Я сделаю только два замечания:
  • Очень неплохо бы заключить все действия, выполняемые скриптом, между функциями gimp-image-undo-group-start и gimp-image-undo-group-end, как это сделано ниже, чтобы пользователю не пришлось отменять каждое действие по-отдельности.
  • Все функции GIMP возвращают результатом списки, независимо от количества данных в результате. Легко проколоться, ожидая, например, layer, а получая (layer). Так что не забывайте делать car в таких случаях.
А теперь пример рабочего кода. Алгоритм я позаимствовал отсюда (спасибо Killy).
(define (script-fu-readability inImage inLayer inRadius inHigh-input)
 (let* (
   (layer2 0)
  )
  (gimp-image-undo-group-start inImage)
  (if (not (= (car (gimp-image-base-type inImage)) GRAY)) (gimp-image-convert-grayscale inImage))
  (set! layer2 (car (gimp-layer-copy inLayer FALSE)))
  (gimp-image-add-layer inImage layer2 -1)
  (plug-in-despeckle RUN-NONINTERACTIVE inImage layer2 inRadius 0 -1 256)
  (gimp-layer-set-mode layer2 DIFFERENCE-MODE)
  (set! inLayer (car (gimp-image-flatten inImage)))
  (gimp-invert inLayer)
  (gimp-levels inLayer HISTOGRAM-VALUE 0 inHigh-input 0.1 0 255)
  (gimp-image-undo-group-end inImage)
 )
)

Имея под рукой браузер процедур, разобраться здесь несложно, если интересно.

Пакетная обработка


Куда-куда? Это ещё не всё. Думаете, мы столько проделали, чтобы написать какой-то несчастный скриптик, обрабатывающий одну картинку? Да руками было бы быстрее! Так что давайте заставим GIMP открыть все файлы из заданной папки, обработать, и сохранить в другую папку.

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

Код частично позаимствован из этого топика (спасибо Apostol), но там он сохраняет файлы, затирая исходные. Функция morph-filename взята отсюда.
(define (morph-filename orig-name new-extension)
 (let* ((buffer (vector "" "" "")))
  (if (re-match "^(.*)[.]([^.]+)$" orig-name buffer)
   (string-append (substring orig-name 0 (car (vector-ref buffer 2))) new-extension)
  )
 )
)

(define (script-fu-batch-readability inInFolder inOutFolder inRadius inHigh-input)
  (let* ((filelist (cadr (file-glob (string-append inInFolder DIR-SEPARATOR "*") 1))))
    (while (not (null? filelist))
      (let* ((filename (car filelist))
          (image (car (gimp-file-load RUN-NONINTERACTIVE filename filename)))
          (layer (car (gimp-image-get-active-layer image)))
        )
        (script-fu-readability image layer inRadius inHigh-input)
        (set! layer (car (gimp-image-get-active-layer image)))
        (set! filename (string-append inOutFolder DIR-SEPARATOR
          (morph-filename (car (gimp-image-get-name image)) "png")))
        (file-png-save2 RUN-NONINTERACTIVE image layer filename filename 0 9 0 0 0 1 0 0 0)
        (gimp-image-delete image)
      )
      (set! filelist (cdr filelist))
    )
  )
)


Заключение


Скрипт Readability вместе с пакетной версией можно скачать здесь (зеркало). Код прокомментирован, даже несколько излишне.

Ещё раз напомню, что статья не претендует на полноту в широком смысле и рассчитана лишь на то, чтобы читатель мог сесть, вдумчиво прочитать, параллельно практикуясь, и начать создавать скрипты, решающие его задачи. Так, чтобы на это не ушло столько же времени, сколько требуется на более-менее качественное изучение языка.

Если вы прочитали статью до конца, то теперь вы умеете Script-fu не хуже меня.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 21

    +1
    Напрашивается фраза вроде «Мое Script-fu круче твоего».
      +4
      Спасибо, хорошая статья. Но есть замечание. Если уже пишете на lisp-подобном языке, то ставьте отступы как принято в lisp'ах, а не в C. Скобки — для синтаксического анализатора, а не для программиста, для них нет смысла выделять отдельные строки:

      (let ((a 1)
            (b 2))
           (+ a b))
      


      Пусть вас не смущает "))))))" в конце строк, это нормально смотрится. Любой редактор с подсветкой скобок поможет понять где чья скобка.
        +4
        И ещё: императивщина!
        (define (difference x y)
          (let ((z (- (if (< x 0) (- x) x)
                      (if (< y 0) (- y) y))))
               (if (< z 0) (- z) z)))
        

          0
          Я тем примером как раз и хотел показать императивные возможности. Потому как картинку проще обрабатывать по шагам, а не в одну строчку.

          За замечания спасибо.
            –1
            Ваш вариант хуже читается и вводит дополнительную переменную, в которой смысла нет.
              0
              Вариант Грибозавра не изменяет аргументы.

              Кстати, Dragonizer, ваша версия функции difference должна называться difference!..
                +1
                Не занком с lisp, параметры передаются по ссылке или по значению? Если второе, то какая разница, изменяются или нет локальные переменные.
                Если первое, то беру свои слова обратно.
                  +1
                  Нет, это я беру свои слова обратно. :)

                  > Не занком с lisp

                  Лиспы бывают разные. В некоторых и по указателю или имени аргументы передаются. Мы говорим о Scheme.
                  0
                  Не совсем понимаю. Здесь же нет никакого стороннего эффекта, нам передалась копия, которую никто кроме нас не видит. Я неправ?
              0
              Ох уж эти требования к скобкам. :)

              Конечно, единые правила оформления это здорово, но мне, например, удобнее редактировать когда вижу где начинается и заканчивается каждый блок. Без прицеливания выделил строки и удалил по Ctrl+D или перенёс куда-нибудь. Или закомментировал.

              Я бы и не против свои скрипты через какой-нибудь beautifier перед публикацией прогонять, но что-то пока ничего понятно-как-работающего (рекомендуют pprint.el) найти не получилось. Может подскажите чего?
              +1
              Много букв и без самого главного — Крутейших картинок с результами.
                +1
                Крутейшие картинки смотрите в упомянутом топике. Алгоритм оттуда, поэтому и результат тот же.
                К тому же, речь шла о приручении языка, а не о решении конкретно моей задачи.
                И еще: букв очень мало для статьи, которая с нуля рассказывает о том, как пользоваться новым языком. Обычно это занимает порядка десятка статей.
                  0
                  Я не сколько не умоляю Ваши заслуги, но речь таки идет о языке Графического редакторе и главный герой здесь — Изображение (не спрятанное где-то в четвертой ссылке, а красной нитью проходящей через повествование).
                    0
                    Спасибо, учту на будущее. Думаю, Вы правы в плане привлечения внимания к статье.
                0
                ВНИМАНИЕ. Если вы в другом источнике встретите противоречащую информацию, верьте ему, а не этой статье. Здесь всё слишком упрощено для новичков.

                Прочитали, попробовали, порадовались что работает, и больше так никогда не пишите.
                  0
                  Вы правильно поняли назначение статьи.
                  Кстати, а что именно прямо-таки противоречит?
                    +1
                    > Переменные в Scheme определяются с помощью конструкции let*

                    Везде в остальных расскажут про let.

                    > Здесь мы использовали функцию if

                    Ранее вы сказали что функция применяется к аргументам, но в данном случае не все аргументы вычислятся. if — это специальная форма, а не функция. Причём чуть раньше set! правильно не называется функцией. :)

                    Про стиль уже сказали, но вот точнейшее описание со всеми подробностями: mumble.net/~campbell/scheme/style.txt Вы правильно обращаетесь со скобками чтобы не пугать народ, но писать так не стоит.
                      +1
                      Большое спасибо за замечания. let* взял из гимпового туториала (ссылка в посте), там другого и не упоминается.
                      Про if — точно, сейчас поправлю одно слово.
                      Стиль взят опять же из гимпового туториала. Да, для снижения порога погружения.

                      Собственно, всё дело в том, что статья про Script-fu в GIMP, а не про Scheme как самостоятельный язык. Для изучения последнего эта статья — далеко не лучший выбор, тогда как для первого вполне сносна.
                      Да, пожалуй, добавлю это замечание в заключение.
                  +1
                  Для пакетной обработки также можно взять плагин для GIMP — BIMP (Batch Image Manipulation), скачать BIMP можно с сайта GIMP

                Only users with full accounts can post comments. Log in, please.