Вступление
В статье будет рассказано о том, как в кратчайшие сроки познакомиться с основами скриптинга в GIMP на языке Scheme и приступить непосредственно к решению простых практических задач. Данный материал предназначен лишь для тех, кто собирается автоматизировать рутинную обработку здесь и сейчас, не сильно вдаваясь в тонкости и не жертвуя драгоценным временем. Также, статью не рекомендуется использовать в качестве пособия по Scheme отдельно от Script-fu. Связано это с упрощённым стилем программирования в данном материале и отсутствием освещения других немаловажных фактов, которые нас сейчас волнуют гораздо меньше, чем скорость освоения.
Содержание:
- Что нам понадобится?
- Коротко о синтаксисе
- Переменные
- Функции
- Списки
- Регистрация скрипта в GIMP
- Написание кода
- Заключение
Что нам понадобится?
Английский интерфейс: для этого достаточно создать переменную окружения «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
в таких случаях.
(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 не хуже меня.