GIMP Script-Fu Первый Дан. Шаг первый
Здравствуй Читатель! Если ты хочешь научиться программировать расширения для GIMP с помощью Script-fu тебе сюда. Я планирую опубликовать серию статей по данной теме. И эта статья только первый шаг в увлекательный мир лайф-кодинга. Что это значит? Расширение GIMP Script-fu представляет собой обёртку над интерпретатором языка scheme, который позволяет не только загружать и интерпретировать скрипты пользователя, но и работать с ними в интерактивном режиме, давая команды интерпретатору в режиме REPL, когда интерпретатор читает код (Read), оценивает (или ещё говорят вычисляет) то что прочитал (Eval), печатает результат (Print) и повторят всё это заново (Loop), короче REPL.
Предисловие
Однажды изучая книгу, Структура и интерпретация компьютерных программ (SICP), я наткнулся на задание по созданию языка функциональной геометрии придуманного Питером Хендерсоном funcgeo2.pdf. И я решил повторить эту реализация в качестве задания, закрепляющего тему процедурной композиции. Обычно задания я делал используя реализацию языка scheme — racket. Отличный язык для обучения основам программирования. Более простой курс этого обучения, пригодный для школьников описан в книге HtDP — «Как разрабатывать программы», перевод которой недавно вышел в печатном виде в России, есть также моя неофициальная попытка перевода, в виде html страниц: htdp-rus. Более продвинутый курс изучения компьютерных наук на языке Scheme описан в уже упомянутой книге SICP, обычно изучаемой уже студентами ВУЗов. Но в языке racket уже реализованы элементы этого языка манипулирования изображениями, поэтому я решил обратить своё внимание на язык script-fu в GIMP, про который мне на тот момент было практически ничего не известно, кроме того что он также является одной из разновидностью реализаций scheme. Результаты попытки реализации этого задания из SCIP и представлены в настоящей работе.
Что такое GIMP
Гимп (GNU Image Manipulation Program), это графический редактор созданный сообществом разработчиков, реализованный на языке программирования Си. Для его создания была создана целая концепция реализации объектно-ориентированного программирования на Си gobject. На его основе разработана библиотека построения графических интерфейсов GTK и даже разработана система управления управления рабочим столом Gnome.
Внешний вид открытого приложения GIMP версии 2.10 выглядит приблизительно так:
Внешний_вид_GIMP
Сам гимп предоставляет богатый набор редактирования изображений, про которые я рассказывать не буду. Но значительная часть функциональности гимп создаётся с помощью расширений, которые могут быть написаны языке Си. Одним из таких расширений является расширение Script-fu позволяющее подключить к гимпу отдельный язык написания расширений, но в отличии от расширений на Си, программы на нём не компилируются, а интерпретируются уже в этом расширении с помощью интерпретатора tinyscheme, присоединённого (подлинкованного) в момент сборки этого расширения. Работа данного плагина доступна через меню: Фильтры→Script-Fu→Консоль. Кстати говоря, в последних версиях гимп разработчики добавили возможность писать плагины с помощью питона, добавив плагин Python-fu. Вот так выглядит окно загруженного плагина консоли Script-fu:
«Окно_script-fu»
Консоль предоставляет возможность вводить команды и вызвать помощь по командам из Script-fu, являющимися фактически интерфейсом для scheme в мир GIMP. Используя эти команды вы можете управлять объектами GIMP.
Что представляет собой TinyScheme.
ТиниСхема (т.е маленькая схема) представляет собой простой интерпретатор языка scheme, написанный на языке Си. Исходные коды тинисхемы доступны вместе с исходниками gimp. Но их можно скачать и отдельно из интернета. Что полезного можно узнать из исходного кода о данной реализации схемы, не погружаясь в глубины Си программирования? Осмотрев файл opdefines.h можно ознакомиться со ВСЕМИ операторами, которые предоставляет интерпретатор языка, всего их около 125 штук. И это ВСЕ операторы реализованные на на си, вернее все к которым можно явно обратиться из программы, помимо них, интерпретатор предоставляет некоторое количество специальных форм, с помощью которых и реализован сам язык, такие как quote, if, let, cond и др. Основные функции языка представлены. А вот чтобы научиться программированию на самом языке вам надо будет ознакомиться какой-нибудь литературой. Та же HtDP прекрасно подойдёт. Так же есть коротенькая книжка TSPL4.
В отличии от полноценных реализаций схемы, таких как racket, guile и др. тинисхема, предоставляет минимальный набор базовых операций и типов данных. Комплексные (составные) типы данных представлены двумя типами данных список и вектор. Простые данные это числа, строки, знаки и символы. Очень интересной возможностью тинисхемы является возможность загрузки специально написанных на Си плагинов (это плагины не к гимпу, а именно к тинисхеме), позволяющая практически неограниченно расширять функциональность тинисхемы.
Начинаем изучение:
Арифметика:
(+ 2 2)
4
(+ 2 2)
4
> (- 34 2 2)
30
> (* 1 2 3 4 5)
120
> (/ 12 4)
3
> (/ 12 5)
2,4.0
> (/ 13 6)
2,166666667.0
> (quotient 27 8)
3
> (remainder 27 8)
3
> (remainder 26 8)
2
> (modulo 23 8)
7
так же реализованы операции exp, log, sin, cos, tan, asin, acos, atan, sqrt, expt, floor, ceiling, truncate, round.
Работа с символьными знаками (charcter).
Почему символьные знаки, а не символы? Дело в том что в Лиспе слово Symbol зарезервировано за специальным типом данных — символами, и что бы не путать их с символьными знаками или просто знаками, я их так назвал.
(char->integer #\a)
;;97
(integer->char 65)
;;#\A#
(char-upcase #\a)
#\A
(char-downcase #\A)
;;#\a
(string #\h #\e #\l #\l #\o)
;;"hello"
;;русский алфавит не поддерживается, не разбирался в чём дело,
;;возможно решается установкой локали.
(string #\п #\р #\и #\в #\е ##\т)
;;Error: undefined sharp expression
`
Есть небольшая проблема с русским языком, но она не столь существенна. На уровне строк, работать с русским языком можно.
Строки.
(make-string 5 #\H)
;;"HHHHH"
(define s1 "Hello World!")
(string-length s1)
;;12
(string-ref s1 3)
;;#\l#
(define s2 "Привет Мир!")
(string-ref s2 0)
;;#\.#
(string-set! s1 3 #\L)
;;Error: string-set!: unable to alter immutable string: "Hello World!"
НЕЛЬЗЯ изменять статически заданную строку.
(define s3 (string #\H #\e #\l #\l #\o))
(string-set! s3 3 #\L)
;;"HelLo"#
s3
;;"HelLo"
(define s4 (string-append "Hello" " " "World" "!"))
;;"Hello World!"
(string-set! s4 3 #\L)
;;"HelLo World!"
(substring s4 6 11)
;;"World"
(string-append "sss" " " "qqq")
;;"sss qqq"
(substring "sss qqq" 4 7)
;;"qqq"
Вывести строку в консоль из программы можно несколькими способами, но самый простой это print.
(print "Привет")
;;"Привет"
Символы.
's1
(define s2 's1)
;;s2
's1
;;s1
s2
;;s1
(symbol? s2)
;;#t
(symbol->string s2)
;;"s1"
(gensym)
;;gensym-3
Составные данные
Пары:
(define p1 (cons 12 'a))
;;p1
(car p1)
12
(cdr p1)
;;a
(set-car! p1 13)
(set-cdr! p1 'b)
;;(13 . a)(13 . b)
p1
;;(13 . b)
Списки:
;;последовательности пар создают списки:
(set-cdr! p1 (cons 'e '()))
;;(13 e)
p1
;;(13 e)
(define l1 '(1 2 3 4 5))
(define l2 (list 'a 'b 'c 'd))
(define l3 (reverse l1))
;;l3
l3
;;(5 4 3 2 1)
l1
;;(1 2 3 4 5)
(define l4 (append l1 l3))
;;l4
l4
;;(1 2 3 4 5 5 4 3 2 1)
(put l4 'a 2)
(get l4 2)
(length l4)
;;10
Ассоциативные списки:
Это списки пар, где первый элемент это ключ, а остаток (cdr) это соответствующее ключу значение, простейшая (вренее медленная) замена хеш-таблицы.
;;alist
(define al1 '((a . 1) (b . 2) (c . 3) (d . 4)))
;;al1
al1
;;((a . 1) (b . 2) (c . 3) (d . 4))
(assq 'c al1)
;;(c . 3)
(define al2 '(("a" . 11) ("b" . 12) ("c" . 13) ("d" . 14)))
(assq "b" al2)
;;#f
(define al3 '((a . 1) (b . 2) (c . 3) (d 4 5 6 7) (e 8 9)))
(assq 'c al3)
;;(c . 3)
(assq 'd al3)
;;(d 4 5 6 7)
Списки свойств(Property List):
Сама тинисхема их поддерживает (несколькими функциями), но чтобы они работали нужно включить опцию USE_PLIST при компиляции, в моей версии script-fu она не включена, но в принципе она и особо не нужна, их сейчас редко кто использует в программировании.
Массивы:
Массивы в тинисхеме представленны в виде одномерных векторов.
(define v1 (vector 12 14 14 'a 'b 'c))
;; v1
v1
;; #( 12 14 14 a b c )
(define v2 (make-vector 12))
#( () () () () () () () () () () () () )
(define v3 (make-vector 12 3))
v3
#( 3 3 3 3 3 3 3 3 3 3 3 3 )
(vector-length v1)
;; 6
(vector-ref v1 3)
;; a
(vector-set! v1 3 'aa)
;;#( 12 14 14 aa b c )
(vector-ref v1 3)
;; aa
Управляющие конструкции.
Условное выполнение — if.
(if (vector? v1)
(print "v1 вектор!!!")
(print "v1 это не вектор!!!"))
;;печать: "v1 вектор!!!"
;;возврат: #t
(set! v1 1)
(if (vector? v1)
(print "v1 вектор!!!")
(print "v1 это не вектор!!!"))
;;"v1 это не вектор!!!"
;;#t
Операция выбора — cond, или множественный if.
Два примерчика:
(define (cond-test1 v1)
(cond ((> v1 10)
(print "больше 10"))
((< v1 10)
(print "меньше 10"))
((= v1 10)
(print "равно 10"))
(#t
(print "незнаю!")))
)
(cond-test1 3)
(cond-test1 11)
(cond-test1 10)
(cond-test1 "12")
;;"меньше 10" #t
;;"больше 10" #t
;;"равно 10" #t
;;Error: >: argument 1 must be: number
(define (cond-test2 v1)
(cond ((and (number? v1) (> v1 10))
(print "больше 10"))
((and (number? v1) (< v1 10))
(print "меньше 10"))
((and (number? v1) (= v1 10))
(print "равно 10"))
(#t
(print "незнаю!")))
)
(cond-test2 3)
(cond-test2 11)
(cond-test2 10)
(cond-test2 "12")
;; "меньше 10" #t
;; "больше 10" #t
;; "равно 10" #t
;; "незнаю!" #t
Перебор случаев — case.
(define (case-test x)
(case x
((5) (print "Угадал! 5"))
((4 6) (print "Не угадал!!!"))
(else (print "Совсем не угадал!"))))
(case-test 4)
(case-test 5)
(case-test 6)
(case-test 9)
;;"Не угадал!!!"
;;"Угадал! 5"
;;"Не угадал!!!"
;;"Совсем не угадал!"
Операторы цикла.
Базовая конструкция цикла в тинисхеме, это безусловный переход на заранее определённую метку. Для этого служит именованный let. Исторически, в схеме, let это аналог определения и вызова лямбды. Но лямбда это неименованная функция, а именованный let, это аналог обычной встроенной (в смысле inline) функции, а метка нужна что бы обратиться к данной функции из любого места этого let, сделать своеобразный вызов, значит перейти по метке, для этого нужно передать в этот вызов, все параметры определённые в let. Давайте посмотрим пример:
;;операторы цикла
(define (string->list2 s)
(let loop1 ((n (pred (string-length s))) (l '()))
(if (= n -1)
l
(loop1 (pred n) (cons (string-ref s n) l)))))
(string->list2 "Hello World!")
;;(#\H #\e #\l #\l #\o #\space #\W #\o #\r #\l #\d #\!)
Здесь loop1 это и есть метка в именованном let. Переход на эту метку, или рекурсия, стоит в конце именованного let, при этом в неё передаётся столько же параметров, сколько было определено в let.
Именованный let вещь понятная, но не всегда удобно её использовать, поэтому в тинисхеме определено несколько макросов циклов while и do.
;; from script-fu-compat.init
(define-macro (while test . body)
`(let loop ()
(cond
(,test
,@body
(loop)
)
)
)
)
(let ((i 0))
(while (< i 5)
(prin1 "i: ") (print i)
(set! i (succ i))))
;;"i: "0
;;"i: "1
;;"i: "2
;;"i: "3
;;"i: "4
;;()
(do ((i 0 (succ i)))
((>= i 5))
(prin1 "i: ") (print i))
;; тот же вывод
Обратите внимание, что в определении while, именованный let снабжён меткой loop, это не какой то магический оператор цикла (loop — петля), это просто метка, названная так для улучшения понимания макроса.
Ну вот, в принципе это и всё богатство данных, функций и операторов, которым снабжает нас тинисхема, для решения наших программных задач. С точки зрения любого другого современного языка программирования это мизер, но тинисхема располагает средством расширения языка, под названием макросы. И это средство, может разрешить большинство проблем, возникающих при программировании на тинисхеме. Остальное, можно решить с помощью написания плагинов к тинисхеме.
Замечательно! Но не находите, что в этой статье я обещал научить программировать в Гимпе, а рассказываю всё про какую-то тинисхему. Пора исправить это недоразумение, пришло время для первой программы работающей с GIMP.
Но об этом я напишу уже в следующей статье…