summaryrefslogtreecommitdiffstats
path: root/plug-ins/pygimp/plug-ins
diff options
context:
space:
mode:
Diffstat (limited to 'plug-ins/pygimp/plug-ins')
-rw-r--r--plug-ins/pygimp/plug-ins/Makefile.am64
-rw-r--r--plug-ins/pygimp/plug-ins/Makefile.in946
-rwxr-xr-xplug-ins/pygimp/plug-ins/benchmark-foreground-extract.py197
-rwxr-xr-xplug-ins/pygimp/plug-ins/clothify.py76
-rwxr-xr-xplug-ins/pygimp/plug-ins/colorxhtml.py212
-rwxr-xr-xplug-ins/pygimp/plug-ins/file-openraster.py404
-rwxr-xr-xplug-ins/pygimp/plug-ins/foggify.py77
-rwxr-xr-xplug-ins/pygimp/plug-ins/gradients-save-as-css.py104
-rwxr-xr-xplug-ins/pygimp/plug-ins/histogram-export.py114
-rw-r--r--plug-ins/pygimp/plug-ins/palette-offset.py61
-rw-r--r--plug-ins/pygimp/plug-ins/palette-sort.py357
-rw-r--r--plug-ins/pygimp/plug-ins/palette-to-gradient.py88
-rwxr-xr-xplug-ins/pygimp/plug-ins/py-slice.py457
-rw-r--r--plug-ins/pygimp/plug-ins/pyconsole.py749
-rwxr-xr-xplug-ins/pygimp/plug-ins/python-console.py245
-rwxr-xr-xplug-ins/pygimp/plug-ins/python-eval.py42
-rwxr-xr-xplug-ins/pygimp/plug-ins/shadow_bevel.py82
-rwxr-xr-xplug-ins/pygimp/plug-ins/sphere.py111
-rw-r--r--plug-ins/pygimp/plug-ins/spyro_plus.py2212
-rwxr-xr-xplug-ins/pygimp/plug-ins/whirlpinch.py227
20 files changed, 6825 insertions, 0 deletions
diff --git a/plug-ins/pygimp/plug-ins/Makefile.am b/plug-ins/pygimp/plug-ins/Makefile.am
new file mode 100644
index 0000000..751f524
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/Makefile.am
@@ -0,0 +1,64 @@
+## Process this file with automake to produce Makefile.in
+
+pluginexecdir = $(gimpplugindir)/plug-ins
+
+source_scripts = \
+ colorxhtml.py \
+ file-openraster.py \
+ foggify.py \
+ gradients-save-as-css.py \
+ histogram-export.py \
+ palette-offset.py \
+ palette-sort.py \
+ palette-to-gradient.py \
+ py-slice.py \
+ python-eval.py \
+ spyro_plus.py \
+ \
+ benchmark-foreground-extract.py \
+ clothify.py \
+ shadow_bevel.py \
+ sphere.py \
+ whirlpinch.py
+
+scripts = \
+ colorxhtml/colorxhtml.py \
+ file-openraster/file-openraster.py \
+ foggify/foggify.py \
+ gradients-save-as-css/gradients-save-as-css.py \
+ histogram-export/histogram-export.py \
+ palette-offset/palette-offset.py \
+ palette-sort/palette-sort.py \
+ palette-to-gradient/palette-to-gradient.py \
+ py-slice/py-slice.py \
+ python-eval/python-eval.py \
+ spyro_plus/spyro_plus.py
+
+test_scripts = \
+ benchmark-foreground-extract/benchmark-foreground-extract.py \
+ clothify/clothify.py \
+ shadow_bevel/shadow_bevel.py \
+ sphere/sphere.py \
+ whirlpinch/whirlpinch.py
+
+$(scripts) $(test_scripts): $(source_scripts)
+ $(AM_V_GEN) mkdir -p $(@D) && cp -f "$(srcdir)/$(@F)" $@
+
+nobase_pluginexec_SCRIPTS = $(scripts)
+
+if GIMP_UNSTABLE
+nobase_pluginexec_SCRIPTS += $(test_scripts)
+endif
+
+# python-console has a data file.
+# Therefore let's move it to its own sub-directory.
+consoleexecdir = $(gimpplugindir)/plug-ins/python-console
+console_scripts = python-console.py
+consoleexec_SCRIPTS = $(console_scripts)
+dist_consoleexec_DATA = pyconsole.py
+
+EXTRA_DIST = \
+ $(source_scripts) \
+ $(console_scripts)
+
+CLEANFILES = $(scripts) $(test_scripts)
diff --git a/plug-ins/pygimp/plug-ins/Makefile.in b/plug-ins/pygimp/plug-ins/Makefile.in
new file mode 100644
index 0000000..a7c7feb
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/Makefile.in
@@ -0,0 +1,946 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@GIMP_UNSTABLE_TRUE@am__append_1 = $(test_scripts)
+subdir = plug-ins/pygimp/plug-ins
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/acinclude.m4 \
+ $(top_srcdir)/m4macros/alsa.m4 \
+ $(top_srcdir)/m4macros/ax_compare_version.m4 \
+ $(top_srcdir)/m4macros/ax_cxx_compile_stdcxx.m4 \
+ $(top_srcdir)/m4macros/ax_gcc_func_attribute.m4 \
+ $(top_srcdir)/m4macros/ax_prog_cc_for_build.m4 \
+ $(top_srcdir)/m4macros/ax_prog_perl_version.m4 \
+ $(top_srcdir)/m4macros/detectcflags.m4 \
+ $(top_srcdir)/m4macros/pythondev.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(dist_consoleexec_DATA) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(consoleexecdir)" \
+ "$(DESTDIR)$(pluginexecdir)" "$(DESTDIR)$(consoleexecdir)"
+SCRIPTS = $(consoleexec_SCRIPTS) $(nobase_pluginexec_SCRIPTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+SOURCES =
+DIST_SOURCES =
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+DATA = $(dist_consoleexec_DATA)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+am__DIST_COMMON = $(srcdir)/Makefile.in
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+AA_LIBS = @AA_LIBS@
+ACLOCAL = @ACLOCAL@
+ALLOCA = @ALLOCA@
+ALL_LINGUAS = @ALL_LINGUAS@
+ALSA_CFLAGS = @ALSA_CFLAGS@
+ALSA_LIBS = @ALSA_LIBS@
+ALTIVEC_EXTRA_CFLAGS = @ALTIVEC_EXTRA_CFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPSTREAM_UTIL = @APPSTREAM_UTIL@
+AR = @AR@
+AS = @AS@
+ATK_CFLAGS = @ATK_CFLAGS@
+ATK_LIBS = @ATK_LIBS@
+ATK_REQUIRED_VERSION = @ATK_REQUIRED_VERSION@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BABL_CFLAGS = @BABL_CFLAGS@
+BABL_LIBS = @BABL_LIBS@
+BABL_REQUIRED_VERSION = @BABL_REQUIRED_VERSION@
+BUG_REPORT_URL = @BUG_REPORT_URL@
+BUILD_EXEEXT = @BUILD_EXEEXT@
+BUILD_OBJEXT = @BUILD_OBJEXT@
+BZIP2_LIBS = @BZIP2_LIBS@
+CAIRO_CFLAGS = @CAIRO_CFLAGS@
+CAIRO_LIBS = @CAIRO_LIBS@
+CAIRO_PDF_CFLAGS = @CAIRO_PDF_CFLAGS@
+CAIRO_PDF_LIBS = @CAIRO_PDF_LIBS@
+CAIRO_PDF_REQUIRED_VERSION = @CAIRO_PDF_REQUIRED_VERSION@
+CAIRO_REQUIRED_VERSION = @CAIRO_REQUIRED_VERSION@
+CATALOGS = @CATALOGS@
+CATOBJEXT = @CATOBJEXT@
+CC = @CC@
+CCAS = @CCAS@
+CCASDEPMODE = @CCASDEPMODE@
+CCASFLAGS = @CCASFLAGS@
+CCDEPMODE = @CCDEPMODE@
+CC_FOR_BUILD = @CC_FOR_BUILD@
+CC_VERSION = @CC_VERSION@
+CFLAGS = @CFLAGS@
+CFLAGS_FOR_BUILD = @CFLAGS_FOR_BUILD@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CPPFLAGS_FOR_BUILD = @CPPFLAGS_FOR_BUILD@
+CPP_FOR_BUILD = @CPP_FOR_BUILD@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DATADIRNAME = @DATADIRNAME@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DESKTOP_DATADIR = @DESKTOP_DATADIR@
+DESKTOP_FILE_VALIDATE = @DESKTOP_FILE_VALIDATE@
+DLLTOOL = @DLLTOOL@
+DOC_SHOOTER = @DOC_SHOOTER@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FILE_AA = @FILE_AA@
+FILE_EXR = @FILE_EXR@
+FILE_HEIF = @FILE_HEIF@
+FILE_JP2_LOAD = @FILE_JP2_LOAD@
+FILE_JPEGXL = @FILE_JPEGXL@
+FILE_MNG = @FILE_MNG@
+FILE_PDF_SAVE = @FILE_PDF_SAVE@
+FILE_PS = @FILE_PS@
+FILE_WMF = @FILE_WMF@
+FILE_XMC = @FILE_XMC@
+FILE_XPM = @FILE_XPM@
+FONTCONFIG_CFLAGS = @FONTCONFIG_CFLAGS@
+FONTCONFIG_LIBS = @FONTCONFIG_LIBS@
+FONTCONFIG_REQUIRED_VERSION = @FONTCONFIG_REQUIRED_VERSION@
+FREETYPE2_REQUIRED_VERSION = @FREETYPE2_REQUIRED_VERSION@
+FREETYPE_CFLAGS = @FREETYPE_CFLAGS@
+FREETYPE_LIBS = @FREETYPE_LIBS@
+GDBUS_CODEGEN = @GDBUS_CODEGEN@
+GDK_PIXBUF_CFLAGS = @GDK_PIXBUF_CFLAGS@
+GDK_PIXBUF_CSOURCE = @GDK_PIXBUF_CSOURCE@
+GDK_PIXBUF_LIBS = @GDK_PIXBUF_LIBS@
+GDK_PIXBUF_REQUIRED_VERSION = @GDK_PIXBUF_REQUIRED_VERSION@
+GEGL = @GEGL@
+GEGL_CFLAGS = @GEGL_CFLAGS@
+GEGL_LIBS = @GEGL_LIBS@
+GEGL_MAJOR_MINOR_VERSION = @GEGL_MAJOR_MINOR_VERSION@
+GEGL_REQUIRED_VERSION = @GEGL_REQUIRED_VERSION@
+GETTEXT_PACKAGE = @GETTEXT_PACKAGE@
+GEXIV2_CFLAGS = @GEXIV2_CFLAGS@
+GEXIV2_LIBS = @GEXIV2_LIBS@
+GEXIV2_REQUIRED_VERSION = @GEXIV2_REQUIRED_VERSION@
+GIMP_API_VERSION = @GIMP_API_VERSION@
+GIMP_APP_VERSION = @GIMP_APP_VERSION@
+GIMP_BINARY_AGE = @GIMP_BINARY_AGE@
+GIMP_COMMAND = @GIMP_COMMAND@
+GIMP_DATA_VERSION = @GIMP_DATA_VERSION@
+GIMP_FULL_NAME = @GIMP_FULL_NAME@
+GIMP_INTERFACE_AGE = @GIMP_INTERFACE_AGE@
+GIMP_MAJOR_VERSION = @GIMP_MAJOR_VERSION@
+GIMP_MICRO_VERSION = @GIMP_MICRO_VERSION@
+GIMP_MINOR_VERSION = @GIMP_MINOR_VERSION@
+GIMP_MKENUMS = @GIMP_MKENUMS@
+GIMP_MODULES = @GIMP_MODULES@
+GIMP_PACKAGE_REVISION = @GIMP_PACKAGE_REVISION@
+GIMP_PKGCONFIG_VERSION = @GIMP_PKGCONFIG_VERSION@
+GIMP_PLUGINS = @GIMP_PLUGINS@
+GIMP_PLUGIN_VERSION = @GIMP_PLUGIN_VERSION@
+GIMP_REAL_VERSION = @GIMP_REAL_VERSION@
+GIMP_RELEASE = @GIMP_RELEASE@
+GIMP_SYSCONF_VERSION = @GIMP_SYSCONF_VERSION@
+GIMP_TOOL_VERSION = @GIMP_TOOL_VERSION@
+GIMP_UNSTABLE = @GIMP_UNSTABLE@
+GIMP_USER_VERSION = @GIMP_USER_VERSION@
+GIMP_VERSION = @GIMP_VERSION@
+GIO_CFLAGS = @GIO_CFLAGS@
+GIO_LIBS = @GIO_LIBS@
+GIO_UNIX_CFLAGS = @GIO_UNIX_CFLAGS@
+GIO_UNIX_LIBS = @GIO_UNIX_LIBS@
+GIO_WINDOWS_CFLAGS = @GIO_WINDOWS_CFLAGS@
+GIO_WINDOWS_LIBS = @GIO_WINDOWS_LIBS@
+GLIB_CFLAGS = @GLIB_CFLAGS@
+GLIB_COMPILE_RESOURCES = @GLIB_COMPILE_RESOURCES@
+GLIB_GENMARSHAL = @GLIB_GENMARSHAL@
+GLIB_LIBS = @GLIB_LIBS@
+GLIB_MKENUMS = @GLIB_MKENUMS@
+GLIB_REQUIRED_VERSION = @GLIB_REQUIRED_VERSION@
+GMODULE_NO_EXPORT_CFLAGS = @GMODULE_NO_EXPORT_CFLAGS@
+GMODULE_NO_EXPORT_LIBS = @GMODULE_NO_EXPORT_LIBS@
+GMOFILES = @GMOFILES@
+GMSGFMT = @GMSGFMT@
+GOBJECT_QUERY = @GOBJECT_QUERY@
+GREP = @GREP@
+GS_LIBS = @GS_LIBS@
+GTKDOC_CHECK = @GTKDOC_CHECK@
+GTKDOC_CHECK_PATH = @GTKDOC_CHECK_PATH@
+GTKDOC_DEPS_CFLAGS = @GTKDOC_DEPS_CFLAGS@
+GTKDOC_DEPS_LIBS = @GTKDOC_DEPS_LIBS@
+GTKDOC_MKPDF = @GTKDOC_MKPDF@
+GTKDOC_REBASE = @GTKDOC_REBASE@
+GTK_CFLAGS = @GTK_CFLAGS@
+GTK_LIBS = @GTK_LIBS@
+GTK_MAC_INTEGRATION_CFLAGS = @GTK_MAC_INTEGRATION_CFLAGS@
+GTK_MAC_INTEGRATION_LIBS = @GTK_MAC_INTEGRATION_LIBS@
+GTK_REQUIRED_VERSION = @GTK_REQUIRED_VERSION@
+GTK_UPDATE_ICON_CACHE = @GTK_UPDATE_ICON_CACHE@
+GUDEV_CFLAGS = @GUDEV_CFLAGS@
+GUDEV_LIBS = @GUDEV_LIBS@
+HARFBUZZ_CFLAGS = @HARFBUZZ_CFLAGS@
+HARFBUZZ_LIBS = @HARFBUZZ_LIBS@
+HARFBUZZ_REQUIRED_VERSION = @HARFBUZZ_REQUIRED_VERSION@
+HAVE_CXX14 = @HAVE_CXX14@
+HAVE_FINITE = @HAVE_FINITE@
+HAVE_ISFINITE = @HAVE_ISFINITE@
+HAVE_VFORK = @HAVE_VFORK@
+HOST_GLIB_COMPILE_RESOURCES = @HOST_GLIB_COMPILE_RESOURCES@
+HTML_DIR = @HTML_DIR@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+INSTOBJEXT = @INSTOBJEXT@
+INTLLIBS = @INTLLIBS@
+INTLTOOL_EXTRACT = @INTLTOOL_EXTRACT@
+INTLTOOL_MERGE = @INTLTOOL_MERGE@
+INTLTOOL_PERL = @INTLTOOL_PERL@
+INTLTOOL_REQUIRED_VERSION = @INTLTOOL_REQUIRED_VERSION@
+INTLTOOL_UPDATE = @INTLTOOL_UPDATE@
+INTLTOOL_V_MERGE = @INTLTOOL_V_MERGE@
+INTLTOOL_V_MERGE_OPTIONS = @INTLTOOL_V_MERGE_OPTIONS@
+INTLTOOL__v_MERGE_ = @INTLTOOL__v_MERGE_@
+INTLTOOL__v_MERGE_0 = @INTLTOOL__v_MERGE_0@
+INTL_MACOSX_LIBS = @INTL_MACOSX_LIBS@
+ISO_CODES_LOCALEDIR = @ISO_CODES_LOCALEDIR@
+ISO_CODES_LOCATION = @ISO_CODES_LOCATION@
+JPEG_LIBS = @JPEG_LIBS@
+JSON_GLIB_CFLAGS = @JSON_GLIB_CFLAGS@
+JSON_GLIB_LIBS = @JSON_GLIB_LIBS@
+JXL_CFLAGS = @JXL_CFLAGS@
+JXL_LIBS = @JXL_LIBS@
+JXL_THREADS_CFLAGS = @JXL_THREADS_CFLAGS@
+JXL_THREADS_LIBS = @JXL_THREADS_LIBS@
+LCMS_CFLAGS = @LCMS_CFLAGS@
+LCMS_LIBS = @LCMS_LIBS@
+LCMS_REQUIRED_VERSION = @LCMS_REQUIRED_VERSION@
+LD = @LD@
+LDFLAGS = @LDFLAGS@
+LDFLAGS_FOR_BUILD = @LDFLAGS_FOR_BUILD@
+LIBBACKTRACE_LIBS = @LIBBACKTRACE_LIBS@
+LIBHEIF_CFLAGS = @LIBHEIF_CFLAGS@
+LIBHEIF_LIBS = @LIBHEIF_LIBS@
+LIBHEIF_REQUIRED_VERSION = @LIBHEIF_REQUIRED_VERSION@
+LIBJXL_REQUIRED_VERSION = @LIBJXL_REQUIRED_VERSION@
+LIBLZMA_REQUIRED_VERSION = @LIBLZMA_REQUIRED_VERSION@
+LIBMYPAINT_CFLAGS = @LIBMYPAINT_CFLAGS@
+LIBMYPAINT_LIBS = @LIBMYPAINT_LIBS@
+LIBMYPAINT_REQUIRED_VERSION = @LIBMYPAINT_REQUIRED_VERSION@
+LIBOBJS = @LIBOBJS@
+LIBPNG_REQUIRED_VERSION = @LIBPNG_REQUIRED_VERSION@
+LIBS = @LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBUNWIND_REQUIRED_VERSION = @LIBUNWIND_REQUIRED_VERSION@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBOBJS = @LTLIBOBJS@
+LT_CURRENT_MINUS_AGE = @LT_CURRENT_MINUS_AGE@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LT_VERSION_INFO = @LT_VERSION_INFO@
+LZMA_CFLAGS = @LZMA_CFLAGS@
+LZMA_LIBS = @LZMA_LIBS@
+MAIL = @MAIL@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MIME_INFO_CFLAGS = @MIME_INFO_CFLAGS@
+MIME_INFO_LIBS = @MIME_INFO_LIBS@
+MIME_TYPES = @MIME_TYPES@
+MKDIR_P = @MKDIR_P@
+MKINSTALLDIRS = @MKINSTALLDIRS@
+MMX_EXTRA_CFLAGS = @MMX_EXTRA_CFLAGS@
+MNG_CFLAGS = @MNG_CFLAGS@
+MNG_LIBS = @MNG_LIBS@
+MSGFMT = @MSGFMT@
+MSGFMT_OPTS = @MSGFMT_OPTS@
+MSGMERGE = @MSGMERGE@
+MYPAINT_BRUSHES_CFLAGS = @MYPAINT_BRUSHES_CFLAGS@
+MYPAINT_BRUSHES_LIBS = @MYPAINT_BRUSHES_LIBS@
+NATIVE_GLIB_CFLAGS = @NATIVE_GLIB_CFLAGS@
+NATIVE_GLIB_LIBS = @NATIVE_GLIB_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OPENEXR_CFLAGS = @OPENEXR_CFLAGS@
+OPENEXR_LIBS = @OPENEXR_LIBS@
+OPENEXR_REQUIRED_VERSION = @OPENEXR_REQUIRED_VERSION@
+OPENJPEG_CFLAGS = @OPENJPEG_CFLAGS@
+OPENJPEG_LIBS = @OPENJPEG_LIBS@
+OPENJPEG_REQUIRED_VERSION = @OPENJPEG_REQUIRED_VERSION@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANGOCAIRO_CFLAGS = @PANGOCAIRO_CFLAGS@
+PANGOCAIRO_LIBS = @PANGOCAIRO_LIBS@
+PANGOCAIRO_REQUIRED_VERSION = @PANGOCAIRO_REQUIRED_VERSION@
+PATHSEP = @PATHSEP@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PERL = @PERL@
+PERL_REQUIRED_VERSION = @PERL_REQUIRED_VERSION@
+PERL_VERSION = @PERL_VERSION@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+PNG_CFLAGS = @PNG_CFLAGS@
+PNG_LIBS = @PNG_LIBS@
+POFILES = @POFILES@
+POPPLER_CFLAGS = @POPPLER_CFLAGS@
+POPPLER_DATA_CFLAGS = @POPPLER_DATA_CFLAGS@
+POPPLER_DATA_LIBS = @POPPLER_DATA_LIBS@
+POPPLER_DATA_REQUIRED_VERSION = @POPPLER_DATA_REQUIRED_VERSION@
+POPPLER_LIBS = @POPPLER_LIBS@
+POPPLER_REQUIRED_VERSION = @POPPLER_REQUIRED_VERSION@
+POSUB = @POSUB@
+PO_IN_DATADIR_FALSE = @PO_IN_DATADIR_FALSE@
+PO_IN_DATADIR_TRUE = @PO_IN_DATADIR_TRUE@
+PYBIN_PATH = @PYBIN_PATH@
+PYCAIRO_CFLAGS = @PYCAIRO_CFLAGS@
+PYCAIRO_LIBS = @PYCAIRO_LIBS@
+PYGIMP_EXTRA_CFLAGS = @PYGIMP_EXTRA_CFLAGS@
+PYGTK_CFLAGS = @PYGTK_CFLAGS@
+PYGTK_CODEGEN = @PYGTK_CODEGEN@
+PYGTK_DEFSDIR = @PYGTK_DEFSDIR@
+PYGTK_LIBS = @PYGTK_LIBS@
+PYLINK_LIBS = @PYLINK_LIBS@
+PYTHON = @PYTHON@
+PYTHON2_REQUIRED_VERSION = @PYTHON2_REQUIRED_VERSION@
+PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@
+PYTHON_INCLUDES = @PYTHON_INCLUDES@
+PYTHON_PLATFORM = @PYTHON_PLATFORM@
+PYTHON_PREFIX = @PYTHON_PREFIX@
+PYTHON_VERSION = @PYTHON_VERSION@
+RANLIB = @RANLIB@
+RSVG_REQUIRED_VERSION = @RSVG_REQUIRED_VERSION@
+RT_LIBS = @RT_LIBS@
+SCREENSHOT_LIBS = @SCREENSHOT_LIBS@
+SED = @SED@
+SENDMAIL = @SENDMAIL@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SOCKET_LIBS = @SOCKET_LIBS@
+SSE2_EXTRA_CFLAGS = @SSE2_EXTRA_CFLAGS@
+SSE4_1_EXTRA_CFLAGS = @SSE4_1_EXTRA_CFLAGS@
+SSE_EXTRA_CFLAGS = @SSE_EXTRA_CFLAGS@
+STRIP = @STRIP@
+SVG_CFLAGS = @SVG_CFLAGS@
+SVG_LIBS = @SVG_LIBS@
+SYMPREFIX = @SYMPREFIX@
+TIFF_LIBS = @TIFF_LIBS@
+USE_NLS = @USE_NLS@
+VERSION = @VERSION@
+WEBKIT_CFLAGS = @WEBKIT_CFLAGS@
+WEBKIT_LIBS = @WEBKIT_LIBS@
+WEBKIT_REQUIRED_VERSION = @WEBKIT_REQUIRED_VERSION@
+WEBPDEMUX_CFLAGS = @WEBPDEMUX_CFLAGS@
+WEBPDEMUX_LIBS = @WEBPDEMUX_LIBS@
+WEBPMUX_CFLAGS = @WEBPMUX_CFLAGS@
+WEBPMUX_LIBS = @WEBPMUX_LIBS@
+WEBP_CFLAGS = @WEBP_CFLAGS@
+WEBP_LIBS = @WEBP_LIBS@
+WEBP_REQUIRED_VERSION = @WEBP_REQUIRED_VERSION@
+WEB_PAGE = @WEB_PAGE@
+WIN32_LARGE_ADDRESS_AWARE = @WIN32_LARGE_ADDRESS_AWARE@
+WINDRES = @WINDRES@
+WMF_CFLAGS = @WMF_CFLAGS@
+WMF_CONFIG = @WMF_CONFIG@
+WMF_LIBS = @WMF_LIBS@
+WMF_REQUIRED_VERSION = @WMF_REQUIRED_VERSION@
+XDG_EMAIL = @XDG_EMAIL@
+XFIXES_CFLAGS = @XFIXES_CFLAGS@
+XFIXES_LIBS = @XFIXES_LIBS@
+XGETTEXT = @XGETTEXT@
+XGETTEXT_REQUIRED_VERSION = @XGETTEXT_REQUIRED_VERSION@
+XMC_CFLAGS = @XMC_CFLAGS@
+XMC_LIBS = @XMC_LIBS@
+XMKMF = @XMKMF@
+XMLLINT = @XMLLINT@
+XMU_LIBS = @XMU_LIBS@
+XPM_LIBS = @XPM_LIBS@
+XSLTPROC = @XSLTPROC@
+XVFB_RUN = @XVFB_RUN@
+X_CFLAGS = @X_CFLAGS@
+X_EXTRA_LIBS = @X_EXTRA_LIBS@
+X_LIBS = @X_LIBS@
+X_PRE_LIBS = @X_PRE_LIBS@
+Z_LIBS = @Z_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CC_FOR_BUILD = @ac_ct_CC_FOR_BUILD@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+gimpdatadir = @gimpdatadir@
+gimpdir = @gimpdir@
+gimplocaledir = @gimplocaledir@
+gimpplugindir = @gimpplugindir@
+gimpsysconfdir = @gimpsysconfdir@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+intltool__v_merge_options_ = @intltool__v_merge_options_@
+intltool__v_merge_options_0 = @intltool__v_merge_options_0@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+manpage_gimpdir = @manpage_gimpdir@
+mkdir_p = @mkdir_p@
+ms_librarian = @ms_librarian@
+mypaint_brushes_dir = @mypaint_brushes_dir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+pkgpyexecdir = @pkgpyexecdir@
+pkgpythondir = @pkgpythondir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+pyexecdir = @pyexecdir@
+pythondir = @pythondir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+pluginexecdir = $(gimpplugindir)/plug-ins
+source_scripts = \
+ colorxhtml.py \
+ file-openraster.py \
+ foggify.py \
+ gradients-save-as-css.py \
+ histogram-export.py \
+ palette-offset.py \
+ palette-sort.py \
+ palette-to-gradient.py \
+ py-slice.py \
+ python-eval.py \
+ spyro_plus.py \
+ \
+ benchmark-foreground-extract.py \
+ clothify.py \
+ shadow_bevel.py \
+ sphere.py \
+ whirlpinch.py
+
+scripts = \
+ colorxhtml/colorxhtml.py \
+ file-openraster/file-openraster.py \
+ foggify/foggify.py \
+ gradients-save-as-css/gradients-save-as-css.py \
+ histogram-export/histogram-export.py \
+ palette-offset/palette-offset.py \
+ palette-sort/palette-sort.py \
+ palette-to-gradient/palette-to-gradient.py \
+ py-slice/py-slice.py \
+ python-eval/python-eval.py \
+ spyro_plus/spyro_plus.py
+
+test_scripts = \
+ benchmark-foreground-extract/benchmark-foreground-extract.py \
+ clothify/clothify.py \
+ shadow_bevel/shadow_bevel.py \
+ sphere/sphere.py \
+ whirlpinch/whirlpinch.py
+
+nobase_pluginexec_SCRIPTS = $(scripts) $(am__append_1)
+
+# python-console has a data file.
+# Therefore let's move it to its own sub-directory.
+consoleexecdir = $(gimpplugindir)/plug-ins/python-console
+console_scripts = python-console.py
+consoleexec_SCRIPTS = $(console_scripts)
+dist_consoleexec_DATA = pyconsole.py
+EXTRA_DIST = \
+ $(source_scripts) \
+ $(console_scripts)
+
+CLEANFILES = $(scripts) $(test_scripts)
+all: all-am
+
+.SUFFIXES:
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu plug-ins/pygimp/plug-ins/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --gnu plug-ins/pygimp/plug-ins/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+install-consoleexecSCRIPTS: $(consoleexec_SCRIPTS)
+ @$(NORMAL_INSTALL)
+ @list='$(consoleexec_SCRIPTS)'; test -n "$(consoleexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(consoleexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(consoleexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ if test -f "$$d$$p"; then echo "$$d$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n' \
+ -e 'h;s|.*|.|' \
+ -e 'p;x;s,.*/,,;$(transform)' | sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1; } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) { files[d] = files[d] " " $$1; \
+ if (++n[d] == $(am__install_max)) { \
+ print "f", d, files[d]; n[d] = 0; files[d] = "" } } \
+ else { print "f", d "/" $$4, $$1 } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_SCRIPT) $$files '$(DESTDIR)$(consoleexecdir)$$dir'"; \
+ $(INSTALL_SCRIPT) $$files "$(DESTDIR)$(consoleexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-consoleexecSCRIPTS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(consoleexec_SCRIPTS)'; test -n "$(consoleexecdir)" || exit 0; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 's,.*/,,;$(transform)'`; \
+ dir='$(DESTDIR)$(consoleexecdir)'; $(am__uninstall_files_from_dir)
+install-nobase_pluginexecSCRIPTS: $(nobase_pluginexec_SCRIPTS)
+ @$(NORMAL_INSTALL)
+ @list='$(nobase_pluginexec_SCRIPTS)'; test -n "$(pluginexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pluginexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pluginexecdir)" || exit 1; \
+ fi; \
+ $(am__nobase_strip_setup); \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ if test -f "$$d$$p"; then echo "$$d$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n' \
+ -e "s|$$srcdirstrip/||" -e 'h;s|[^/]*$$||; s|^$$|.|' \
+ -e 'p;x;s,.*/,,;$(transform)' | sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1; } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) { files[d] = files[d] " " $$1; \
+ if (++n[d] == $(am__install_max)) { \
+ print "f", d, files[d]; n[d] = 0; files[d] = "" } } \
+ else { print "f", d "/" $$4, $$1 } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ case $$type in \
+ d) echo " $(MKDIR_P) '$(DESTDIR)$(pluginexecdir)/$$dir'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pluginexecdir)/$$dir" || exit $$?;; \
+ f) \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_SCRIPT) $$files '$(DESTDIR)$(pluginexecdir)$$dir'"; \
+ $(INSTALL_SCRIPT) $$files "$(DESTDIR)$(pluginexecdir)$$dir" || exit $$?; \
+ } \
+ ;; esac \
+ ; done
+
+uninstall-nobase_pluginexecSCRIPTS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(nobase_pluginexec_SCRIPTS)'; test -n "$(pluginexecdir)" || exit 0; \
+ $(am__nobase_strip_setup); \
+ files=`$(am__nobase_strip) \
+ -e 'h;s,.*/,,;$(transform);x;s|[^/]*$$||;G;s,\n,,'`; \
+ dir='$(DESTDIR)$(pluginexecdir)'; $(am__uninstall_files_from_dir)
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-dist_consoleexecDATA: $(dist_consoleexec_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_consoleexec_DATA)'; test -n "$(consoleexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(consoleexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(consoleexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(consoleexecdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(consoleexecdir)" || exit $$?; \
+ done
+
+uninstall-dist_consoleexecDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_consoleexec_DATA)'; test -n "$(consoleexecdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(consoleexecdir)'; $(am__uninstall_files_from_dir)
+tags TAGS:
+
+ctags CTAGS:
+
+cscope cscopelist:
+
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(SCRIPTS) $(DATA)
+installdirs:
+ for dir in "$(DESTDIR)$(consoleexecdir)" "$(DESTDIR)$(pluginexecdir)" "$(DESTDIR)$(consoleexecdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+ -test -z "$(CLEANFILES)" || rm -f $(CLEANFILES)
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool mostlyclean-am
+
+distclean: distclean-am
+ -rm -f Makefile
+distclean-am: clean-am distclean-generic
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-consoleexecSCRIPTS \
+ install-dist_consoleexecDATA install-nobase_pluginexecSCRIPTS
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-generic mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-consoleexecSCRIPTS \
+ uninstall-dist_consoleexecDATA \
+ uninstall-nobase_pluginexecSCRIPTS
+
+.MAKE: install-am install-strip
+
+.PHONY: all all-am check check-am clean clean-generic clean-libtool \
+ cscopelist-am ctags-am distclean distclean-generic \
+ distclean-libtool distdir dvi dvi-am html html-am info info-am \
+ install install-am install-consoleexecSCRIPTS install-data \
+ install-data-am install-dist_consoleexecDATA install-dvi \
+ install-dvi-am install-exec install-exec-am install-html \
+ install-html-am install-info install-info-am install-man \
+ install-nobase_pluginexecSCRIPTS install-pdf install-pdf-am \
+ install-ps install-ps-am install-strip installcheck \
+ installcheck-am installdirs maintainer-clean \
+ maintainer-clean-generic mostlyclean mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags-am uninstall \
+ uninstall-am uninstall-consoleexecSCRIPTS \
+ uninstall-dist_consoleexecDATA \
+ uninstall-nobase_pluginexecSCRIPTS
+
+.PRECIOUS: Makefile
+
+
+$(scripts) $(test_scripts): $(source_scripts)
+ $(AM_V_GEN) mkdir -p $(@D) && cp -f "$(srcdir)/$(@F)" $@
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/plug-ins/pygimp/plug-ins/benchmark-foreground-extract.py b/plug-ins/pygimp/plug-ins/benchmark-foreground-extract.py
new file mode 100755
index 0000000..2637547
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/benchmark-foreground-extract.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python2
+
+# Foreground Extraction Benchmark
+# Copyright 2005 Sven Neumann <sven@gimp.org>
+#
+# This is a from-scratch implementation of the benchmark proposed in
+# "GrabCut": interactive foreground extraction using iterated graph
+# cuts published in the Proceedings of the 2004 SIGGRAPH Conference.
+#
+# No guarantee is made that this benchmark produces the same results
+# as the cited benchmark but the goal is that it does. So if you find
+# any bugs or inaccuracies in this code, please let us know.
+#
+# The benchmark has been adapted work with the SIOX algorithm
+# (http://www.siox.org). which is (currently) the only
+# implementation of gimp_drawable_foreground_extract(). If other
+# implementations are being added, this benchmark should be changed
+# accordingly.
+#
+# You will need a set of test images to run this benchmark, preferably
+# the original set of 50 images. Some of these images are from the
+# Berkeley Segmentation Dataset
+# (http://www.cs.berkeley.edu/projects/vision/grouping/segbench/).
+# See also http://www.siox.org/details.html for trimaps.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+
+import os, re, struct, sys, time
+
+from gimpfu import *
+
+
+def benchmark (folder, save_output):
+ folder = os.path.abspath (folder)
+ if not os.path.exists (folder):
+ gimp.message("Folder '" + folder + "' doesn't exist.\n")
+ return;
+
+ total_unclassified = 0
+ total_misclassified = 0
+ total_time = 0.0
+
+ images = os.path.join (folder, "images")
+ for name in os.listdir (images):
+
+ try:
+ gimp.delete (image_display)
+ gimp.delete (mask_display)
+ except UnboundLocalError:
+ pass
+
+ image_name = os.path.join (images, name)
+
+ # FIXME: improve this!
+ name = re.sub (r'\.jpg$', '', name)
+ name = re.sub (r'\.JPG$', '', name)
+ name = re.sub (r'\.bmp$', '', name)
+
+ mask_name = os.path.join (folder, "cm_bmp", name + '.png')
+ truth_name = os.path.join (folder, "truth", name + '.bmp')
+
+ image = pdb.gimp_file_load (image_name, image_name)
+ image_layer = image.active_layer;
+
+ mask = pdb.gimp_file_load (mask_name, mask_name)
+ convert_grayscale (mask)
+ mask_layer = mask.active_layer;
+
+ truth = pdb.gimp_file_load (truth_name, truth_name)
+ convert_grayscale (truth)
+ truth_layer = truth.active_layer;
+
+ unclassified = unclassified_pixels (mask_layer, truth_layer)
+
+ sys.stderr.write (os.path.basename (image_name))
+
+ start = time.time ()
+ pdb.gimp_drawable_foreground_extract (image_layer,
+ FOREGROUND_EXTRACT_SIOX,
+ mask_layer)
+ end = time.time ()
+
+ sys.stderr.write (" ")
+
+ mask_layer.flush ()
+
+ # Ignore errors when creating image displays;
+ # allows us to be used without a display.
+ try:
+ image_display = pdb.gimp_display_new (image)
+ mask_display = pdb.gimp_display_new (mask)
+
+ gimp.displays_flush ()
+ time.sleep (1.0)
+ except:
+ pass
+
+ gimp.delete (image)
+
+ misclassified = misclassified_pixels (mask_layer, truth_layer)
+
+ sys.stderr.write ("%d %d %.2f%% %.3fs\n" %
+ (unclassified, misclassified,
+ (misclassified * 100.0 / unclassified),
+ end - start))
+
+ total_unclassified += unclassified
+ total_misclassified += misclassified
+ total_time += end - start
+
+ gimp.delete (truth)
+
+ if save_output:
+ filename = os.path.join (folder, "output", name + '.png')
+ pdb.gimp_file_save (mask, mask_layer, filename, filename)
+
+ gimp.delete (mask)
+
+ # for loop ends
+
+ try:
+ gimp.delete (image_display)
+ gimp.delete (mask_display)
+ except UnboundLocalError:
+ pass
+
+ sys.stderr.write ("Total: %d %d %.2f%% %.3fs\n" %
+ (total_unclassified, total_misclassified,
+ (total_misclassified * 100.0 / total_unclassified),
+ total_time))
+
+def convert_grayscale (image):
+ if image.base_type != GRAY:
+ pdb.gimp_image_convert_grayscale (image)
+
+
+def unclassified_pixels (mask, truth):
+ (mean, std_dev, median, pixels,
+ count, percentile) = pdb.gimp_histogram (mask, HISTOGRAM_VALUE, 1, 254)
+
+ return count
+
+
+def misclassified_pixels (mask, truth):
+ image = truth.image
+
+ copy = pdb.gimp_layer_new_from_drawable (mask, image)
+ copy.name = "Difference"
+ copy.mode = DIFFERENCE_MODE
+
+ image.insert_layer (copy)
+
+ # The assumption made here is that the output of
+ # foreground_extract is a strict black and white mask. The truth
+ # however may contain unclassified pixels. These are considered
+ # unknown, a strict segmentation isn't possible here.
+ #
+ # The result of using the Difference mode as done here is that
+ # pure black pixels in the result can be considered correct.
+ # White pixels are wrong. Gray values were unknown in the truth
+ # and thus are not counted as wrong.
+
+ (mean, std_dev, median, pixels,
+ count, percentile) = pdb.gimp_histogram (image.flatten (),
+ HISTOGRAM_VALUE, 255, 255)
+
+ return count
+
+
+register (
+ "python-fu-benchmark-foreground-extract",
+ "Benchmark and regression test for the SIOX algorithm",
+ "",
+ "Sven Neumann",
+ "Sven Neumann",
+ "2005",
+ "Foreground Extraction",
+ "",
+ [ (PF_FILE, "image-folder", "Image folder",
+ "~/segmentation/msbench/imagedata"),
+ (PF_TOGGLE, "save-output", "Save output images", False) ],
+ [],
+ benchmark, menu="<Image>/Filters/Extensions/Benchmark")
+
+main ()
diff --git a/plug-ins/pygimp/plug-ins/clothify.py b/plug-ins/pygimp/plug-ins/clothify.py
new file mode 100755
index 0000000..258329c
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/clothify.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python2
+
+# Gimp-Python - allows the writing of Gimp plugins in Python.
+# Copyright (C) 1997 James Henstridge <james@daa.com.au>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import math
+from gimpfu import *
+
+def clothify(timg, tdrawable, bx=9, by=9, azimuth=135, elevation=45, depth=3):
+ width = tdrawable.width
+ height = tdrawable.height
+
+ img = gimp.Image(width, height, RGB)
+ img.disable_undo()
+
+ layer_one = gimp.Layer(img, "X Dots", width, height, RGB_IMAGE,
+ 100, NORMAL_MODE)
+ img.insert_layer(layer_one)
+ pdb.gimp_edit_fill(layer_one, BACKGROUND_FILL)
+
+ pdb.plug_in_noisify(img, layer_one, 0, 0.7, 0.7, 0.7, 0.7)
+
+ layer_two = layer_one.copy()
+ layer_two.mode = MULTIPLY_MODE
+ layer_two.name = "Y Dots"
+ img.insert_layer(layer_two)
+
+ pdb.plug_in_gauss_rle(img, layer_one, bx, 1, 0)
+ pdb.plug_in_gauss_rle(img, layer_two, by, 0, 1)
+
+ img.flatten()
+
+ bump_layer = img.active_layer
+
+ pdb.plug_in_c_astretch(img, bump_layer)
+ pdb.plug_in_noisify(img, bump_layer, 0, 0.2, 0.2, 0.2, 0.2)
+ pdb.plug_in_bump_map(img, tdrawable, bump_layer, azimuth,
+ elevation, depth, 0, 0, 0, 0, True, False, 0)
+
+ gimp.delete(img)
+
+register(
+ "python-fu-clothify",
+ "Make the image look like it is printed on cloth",
+ "Make the specified layer look like it is printed on cloth",
+ "James Henstridge",
+ "James Henstridge",
+ "1997-1999",
+ "_Clothify...",
+ "RGB*, GRAY*",
+ [
+ (PF_IMAGE, "image", "Input image", None),
+ (PF_DRAWABLE, "drawable", "Input drawable", None),
+ (PF_INT, "x-blur", "X blur", 9),
+ (PF_INT, "y-blur", "Y blur", 9),
+ (PF_INT, "azimuth", "Azimuth", 135),
+ (PF_INT, "elevation", "Elevation", 45),
+ (PF_INT, "depth", "Depth", 3)
+ ],
+ [],
+ clothify, menu="<Image>/Filters/Artistic")
+
+main()
diff --git a/plug-ins/pygimp/plug-ins/colorxhtml.py b/plug-ins/pygimp/plug-ins/colorxhtml.py
new file mode 100755
index 0000000..011f8ca
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/colorxhtml.py
@@ -0,0 +1,212 @@
+#!/usr/bin/env python2
+
+# Gimp-Python - allows the writing of Gimp plugins in Python.
+# Copyright (C) 2003, 2005 Manish Singh <yosh@gimp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import string
+import struct
+import inspect
+import os.path
+
+import gimp
+from gimpfu import *
+
+gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
+
+all_source_types = (CHARS_SOURCE, CHARS_FILE, CHARS_PARAMETER) = range(3)
+
+escape_table = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ '"': '&quot;'
+}
+
+style_def = """body {
+ width: 100%%;
+ font-size: %dpx;
+ background-color: #000000;
+ color: #ffffff;
+}
+"""
+
+preamble = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+<title>CSS Color XHTML written by GIMP</title>
+%s
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+</head>
+<body>
+<pre>
+"""
+
+postamble = """\n</pre>\n</body>\n</html>\n"""
+
+def colorxhtml(img, drawable, filename, raw_filename,
+ source_type, characters, size, separate):
+ width = drawable.width
+ height = drawable.height
+ bpp = drawable.bpp
+
+ if not drawable.is_rgb or drawable.has_alpha:
+ return
+
+ if source_type not in all_source_types:
+ return
+
+ gimp.tile_cache_ntiles(width / gimp.tile_width() + 1)
+
+ html = file(filename, 'w')
+
+ if separate:
+ dirname, cssfile = os.path.split(filename)
+ cssfile = os.path.splitext(cssfile)[0] + '.css'
+ cssname = os.path.join(dirname, cssfile)
+
+ css = file(cssname, 'w')
+
+ if source_type == CHARS_SOURCE:
+ chars = file(inspect.getsourcefile(colorxhtml)).read()
+ elif source_type == CHARS_FILE:
+ chars = file(characters).read()
+ elif source_type == CHARS_PARAMETER:
+ chars = characters
+
+ allchars = string.maketrans('', '')
+
+ goodchars = string.digits + string.ascii_letters + string.punctuation
+ badchars = ''.join([c for c in allchars if c not in goodchars])
+
+ chars = chars.translate(allchars, badchars)
+
+ data = []
+
+ for c in chars:
+ data.append(escape_table.get(c, c))
+
+ if data:
+ data.reverse()
+ else:
+ data = list('X' * 80)
+
+ pr = drawable.get_pixel_rgn(0, 0, width, height, False, False)
+
+ gimp.progress_init(_("Saving as colored XHTML"))
+
+ style = style_def % size
+
+ if separate:
+ ss = '<link rel="stylesheet" type="text/css" href="%s" />' % cssfile
+ css.write(style)
+ else:
+ ss = '<style type="text/css">\n%s</style>' % style
+
+ html.write(preamble % ss)
+
+ colors = {}
+ chars = []
+
+ for y in range(0, height):
+ row = pr[0:width, y]
+
+ while len(chars) < width:
+ chars[0:0] = data
+
+ for pixel in RowIterator(row, bpp):
+ color = '%02x%02x%02x' % pixel
+ style = 'background-color:black; color:#%s;' % color
+ char = chars.pop()
+
+ if separate:
+ if color not in colors:
+ css.write('span.N%s { %s }\n' % (color, style))
+ colors[color] = 1
+
+ html.write('<span class="N%s">%s</span>' % (color, char))
+
+ else:
+ html.write('<span style="%s">%s</span>' % (style, char))
+
+ html.write('\n')
+
+ gimp.progress_update(y / float(height))
+
+ html.write(postamble)
+
+ html.close()
+
+ if separate:
+ css.close()
+
+def register_save():
+ gimp.register_save_handler("file-colorxhtml-save", "xhtml", "")
+
+class RowIterator:
+ def __init__(self, row, bpp):
+ self.row = row
+ self.bpp = bpp
+
+ self.start = 0
+ self.stop = bpp
+
+ self.length = len(row)
+ self.fmt = 'B' * bpp
+
+ def __iter__(self):
+ return iter(self.get_pixel, None)
+
+ def get_pixel(self):
+ if self.stop > self.length:
+ return None
+
+ pixel = struct.unpack(self.fmt, self.row[self.start:self.stop])
+
+ self.start += self.bpp
+ self.stop += self.bpp
+
+ return pixel
+
+register(
+ "file-colorxhtml-save",
+ N_("Save as colored XHTML"),
+ "Saves the image as colored XHTML text (based on Perl version by Marc Lehmann)",
+ "Manish Singh and Carol Spears",
+ "Manish Singh and Carol Spears",
+ "2003",
+ N_("Colored XHTML"),
+ "RGB",
+ [
+ (PF_IMAGE, "image", "Input image", None),
+ (PF_DRAWABLE, "drawable", "Input drawable", None),
+ (PF_STRING, "filename", "The name of the file", None),
+ (PF_STRING, "raw-filename", "The name of the file", None),
+ (PF_RADIO, "source", _("Character _source"), 0,
+ ((_("Source code"), CHARS_SOURCE),
+ (_("Text file"), CHARS_FILE),
+ (_("Entry box"), CHARS_PARAMETER))),
+ (PF_FILE, "characters", _("_File to read or characters to use"),
+ ""),
+ (PF_INT, "font-size", _("Fo_nt size in pixels"), 10),
+ (PF_BOOL, "separate", _("_Write a separate CSS file"), True)
+ ],
+ [],
+ colorxhtml, on_query=register_save,
+ menu="<Save>", domain=("gimp20-python", gimp.locale_directory)
+ )
+
+main()
diff --git a/plug-ins/pygimp/plug-ins/file-openraster.py b/plug-ins/pygimp/plug-ins/file-openraster.py
new file mode 100755
index 0000000..c55b1b6
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/file-openraster.py
@@ -0,0 +1,404 @@
+#!/usr/bin/env python2
+
+# GIMP Plug-in for the OpenRaster file format
+# http://create.freedesktop.org/wiki/OpenRaster
+
+# Copyright (C) 2009 by Jon Nordby <jononor@gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Based on MyPaint source code by Martin Renold
+# http://gitorious.org/mypaint/mypaint/blobs/edd84bcc1e091d0d56aa6d26637aa8a925987b6a/lib/document.py
+
+import os, sys, tempfile, zipfile
+import xml.etree.ElementTree as ET
+
+
+from gimpfu import *
+
+NESTED_STACK_END = object()
+
+
+layermodes_map = {
+ "svg:src-over": LAYER_MODE_NORMAL,
+ "svg:multiply": LAYER_MODE_MULTIPLY_LEGACY,
+ "svg:screen": LAYER_MODE_SCREEN_LEGACY,
+ "svg:overlay": LAYER_MODE_OVERLAY,
+ "svg:darken": LAYER_MODE_DARKEN_ONLY_LEGACY,
+ "svg:lighten": LAYER_MODE_LIGHTEN_ONLY_LEGACY,
+ "svg:color-dodge": LAYER_MODE_DODGE_LEGACY,
+ "svg:color-burn": LAYER_MODE_BURN_LEGACY,
+ "svg:hard-light": LAYER_MODE_HARDLIGHT_LEGACY,
+ "svg:soft-light": LAYER_MODE_SOFTLIGHT_LEGACY,
+ "svg:difference": LAYER_MODE_DIFFERENCE_LEGACY,
+ "svg:color": LAYER_MODE_HSL_COLOR_LEGACY,
+ "svg:luminosity": LAYER_MODE_HSV_VALUE_LEGACY,
+ "svg:hue": LAYER_MODE_HSV_HUE_LEGACY,
+ "svg:saturation": LAYER_MODE_HSV_SATURATION_LEGACY,
+ "svg:plus": LAYER_MODE_ADDITION_LEGACY,
+}
+
+def reverse_map(mapping):
+ return dict((v,k) for k, v in mapping.iteritems())
+
+def get_image_attributes(orafile):
+ xml = orafile.read('stack.xml')
+ image = ET.fromstring(xml)
+ stack = image.find('stack')
+ w = int(image.attrib.get('w', ''))
+ h = int(image.attrib.get('h', ''))
+
+ return stack, w, h
+
+def get_layer_attributes(layer):
+ a = layer.attrib
+ path = a.get('src', '')
+ name = a.get('name', '')
+ x = int(a.get('x', '0'))
+ y = int(a.get('y', '0'))
+ opac = float(a.get('opacity', '1.0'))
+ visible = a.get('visibility', 'visible') != 'hidden'
+ m = a.get('composite-op', 'svg:src-over')
+ layer_mode = layermodes_map.get(m, LAYER_MODE_NORMAL)
+
+ return path, name, x, y, opac, visible, layer_mode
+
+def get_group_layer_attributes(layer):
+ a = layer.attrib
+ name = a.get('name', '')
+ opac = float(a.get('opacity', '1.0'))
+ visible = a.get('visibility', 'visible') != 'hidden'
+ m = a.get('composite-op', 'svg:src-over')
+ layer_mode = layermodes_map.get(m, NORMAL_MODE)
+
+ return name, 0, 0, opac, visible, layer_mode
+
+def thumbnail_ora(filename, thumb_size):
+ # FIXME: Untested. Does not seem to be used at all? should be run
+ # when registered and there is no thumbnail in cache
+ tempdir = tempfile.mkdtemp('gimp-plugin-file-openraster')
+ original_name = filename
+ try:
+ if not isinstance(filename, str):
+ filename = filename.decode("utf-8")
+ orafile = zipfile.ZipFile(filename.encode(sys.getfilesystemencoding() or "utf-8"))
+ except (UnicodeDecodeError, IOError):
+ # Someone may try to open an actually garbled name, and pass a raw
+ # non-utf 8 filename:
+ orafile = zipfile.ZipFile(original_name)
+ orafile = zipfile.ZipFile(filename)
+ stack, w, h = get_image_attributes(orafile)
+
+ # create temp file
+ tmp = os.path.join(tempdir, 'tmp.png')
+ f = open(tmp, 'wb')
+ f.write(orafile.read('Thumbnails/thumbnail.png'))
+ f.close()
+
+ img = pdb['file-png-load'](tmp, 'tmp.png')
+ # TODO: scaling
+ os.remove(tmp)
+ os.rmdir(tempdir)
+
+ return (img, w, h)
+
+def save_ora(img, drawable, filename, raw_filename):
+ def write_file_str(zfile, fname, data):
+ # work around a permission bug in the zipfile library:
+ # http://bugs.python.org/issue3394
+ zi = zipfile.ZipInfo(fname)
+ zi.external_attr = int("100644", 8) << 16
+ zfile.writestr(zi, data)
+
+ tempdir = tempfile.mkdtemp('gimp-plugin-file-openraster')
+
+ if isinstance(filename, str):
+ try:
+ filename = filename.decode("utf-8")
+ except UnicodeDecodeError:
+ # 1 - 1 correspondence between raw_bytes and UCS-2 used by Python
+ # Unicode characters
+ filename = filename.decode("latin1")
+ encoding = sys.getfilesystemencoding() or "utf-8"
+ filename = filename.encode(encoding)
+ tmp_sufix = ".tmpsave".encode(encoding)
+ # use .tmpsave extension, so we don't overwrite a valid file if
+ # there is an exception
+ orafile = zipfile.ZipFile(filename + tmp_sufix, 'w', compression=zipfile.ZIP_STORED)
+
+ write_file_str(orafile, 'mimetype', 'image/openraster') # must be the first file written
+
+ # build image attributes
+ image = ET.Element('image')
+ stack = ET.SubElement(image, 'stack')
+ a = image.attrib
+ a['w'] = str(img.width)
+ a['h'] = str(img.height)
+
+ def store_layer(img, drawable, path):
+ tmp = os.path.join(tempdir, 'tmp.png')
+ interlace, compression = 0, 2
+ png_chunks = (1, 1, 0, 1, 1) # write all PNG chunks except oFFs(ets)
+ pdb['file-png-save'](img, drawable, tmp, 'tmp.png',
+ interlace, compression, *png_chunks)
+ orafile.write(tmp, path)
+ os.remove(tmp)
+
+ def add_layer(parent, x, y, opac, gimp_layer, path, visible=True):
+ store_layer(img, gimp_layer, path)
+ # create layer attributes
+ layer = ET.Element('layer')
+ parent.append(layer)
+ a = layer.attrib
+ a['src'] = path
+ a['name'] = gimp_layer.name
+ a['x'] = str(x)
+ a['y'] = str(y)
+ a['opacity'] = str(opac)
+ a['visibility'] = 'visible' if visible else 'hidden'
+ a['composite-op'] = reverse_map(layermodes_map).get(gimp_layer.mode, 'svg:src-over')
+ return layer
+
+ def add_group_layer(parent, opac, gimp_layer, visible=True):
+ # create layer attributes
+ group_layer = ET.Element('stack')
+ parent.append(group_layer)
+ a = group_layer.attrib
+ a['name'] = gimp_layer.name
+ a['opacity'] = str(opac)
+ a['visibility'] = 'visible' if visible else 'hidden'
+ a['composite-op'] = reverse_map(layermodes_map).get(gimp_layer.mode, 'svg:src-over')
+ return group_layer
+
+
+ def enumerate_layers(group):
+ for layer in group.layers:
+ if not isinstance(layer, gimp.GroupLayer):
+ yield layer
+ else:
+ yield layer
+ for sublayer in enumerate_layers(layer):
+ yield sublayer
+ yield NESTED_STACK_END
+
+ # save layers
+ parent_groups = []
+ i = 0
+ for lay in enumerate_layers(img):
+ if lay is NESTED_STACK_END:
+ parent_groups.pop()
+ continue
+ x, y = lay.offsets
+ opac = lay.opacity / 100.0 # needs to be between 0.0 and 1.0
+
+ if not parent_groups:
+ path_name = 'data/{:03d}.png'.format(i)
+ i += 1
+ else:
+ path_name = 'data/{}-{:03d}.png'.format(
+ parent_groups[-1][1], parent_groups[-1][2])
+ parent_groups[-1][2] += 1
+
+ parent = stack if not parent_groups else parent_groups[-1][0]
+
+ if isinstance(lay, gimp.GroupLayer):
+ group = add_group_layer(parent, opac, lay, lay.visible)
+ group_path = ("{:03d}".format(i) if not parent_groups else
+ parent_groups[-1][1] + "-{:03d}".format(parent_groups[-1][2]))
+ parent_groups.append([group, group_path , 0])
+ else:
+ add_layer(parent, x, y, opac, lay, path_name, lay.visible)
+
+ # save mergedimage
+ thumb = pdb['gimp-image-duplicate'](img)
+ thumb_layer = thumb.merge_visible_layers (CLIP_TO_IMAGE)
+ store_layer (thumb, thumb_layer, 'mergedimage.png')
+
+ # save thumbnail
+ w, h = img.width, img.height
+ if max (w, h) > 256:
+ # should be at most 256x256, without changing aspect ratio
+ if w > h:
+ w, h = 256, max(h*256/w, 1)
+ else:
+ w, h = max(w*256/h, 1), 256
+ thumb_layer.scale(w, h)
+ if thumb.precision != PRECISION_U8_GAMMA:
+ pdb.gimp_image_convert_precision (thumb, PRECISION_U8_GAMMA)
+ store_layer(thumb, thumb_layer, 'Thumbnails/thumbnail.png')
+ gimp.delete(thumb)
+
+ # write stack.xml
+ xml = ET.tostring(image, encoding='UTF-8')
+ write_file_str(orafile, 'stack.xml', xml)
+
+ # finish up
+ orafile.close()
+ os.rmdir(tempdir)
+ if os.path.exists(filename):
+ os.remove(filename) # win32 needs that
+ os.rename(filename + tmp_sufix, filename)
+
+
+def load_ora(filename, raw_filename):
+ tempdir = tempfile.mkdtemp('gimp-plugin-file-openraster')
+ original_name = filename
+ try:
+ if not isinstance(filename, str):
+ filename = filename.decode("utf-8")
+ orafile = zipfile.ZipFile(filename.encode(sys.getfilesystemencoding() or "utf-8"))
+ except (UnicodeDecodeError, IOError):
+ # Someone may try to open an actually garbled name, and pass a raw
+ # non-utf 8 filename:
+ orafile = zipfile.ZipFile(original_name)
+ stack, w, h = get_image_attributes(orafile)
+
+ img = gimp.Image(w, h, RGB)
+ img.filename = filename
+
+ def get_layers(root):
+ """iterates over layers and nested stacks"""
+ for item in root:
+ if item.tag == 'layer':
+ yield item
+ elif item.tag == 'stack':
+ yield item
+ for subitem in get_layers(item):
+ yield subitem
+ yield NESTED_STACK_END
+
+ parent_groups = []
+
+ layer_no = 0
+ for item in get_layers(stack):
+
+ if item is NESTED_STACK_END:
+ parent_groups.pop()
+ continue
+
+ if item.tag == 'stack':
+ name, x, y, opac, visible, layer_mode = get_group_layer_attributes(item)
+ gimp_layer = gimp.GroupLayer(img)
+
+ else:
+ path, name, x, y, opac, visible, layer_mode = get_layer_attributes(item)
+
+ if not path.lower().endswith('.png'):
+ continue
+ if not name:
+ # use the filename without extension as name
+ n = os.path.basename(path)
+ name = os.path.splitext(n)[0]
+
+ # create temp file. Needed because gimp cannot load files from inside a zip file
+ tmp = os.path.join(tempdir, 'tmp.png')
+ f = open(tmp, 'wb')
+ try:
+ data = orafile.read(path)
+ except KeyError:
+ # support for bad zip files (saved by old versions of this plugin)
+ data = orafile.read(path.encode('utf-8'))
+ print 'WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(path)
+ f.write(data)
+ f.close()
+
+ # import layer, set attributes and add to image
+ gimp_layer = pdb['gimp-file-load-layer'](img, tmp)
+ os.remove(tmp)
+ gimp_layer.name = name
+ gimp_layer.mode = layer_mode
+ gimp_layer.set_offsets(x, y) # move to correct position
+ gimp_layer.opacity = opac * 100 # a float between 0 and 100
+ gimp_layer.visible = visible
+
+ pdb.gimp_image_insert_layer(img, gimp_layer,
+ parent_groups[-1][0] if parent_groups else None,
+ parent_groups[-1][1] if parent_groups else layer_no)
+ if parent_groups:
+ parent_groups[-1][1] += 1
+ else:
+ layer_no += 1
+
+ if isinstance(gimp_layer, gimp.GroupLayer):
+ parent_groups.append([gimp_layer, 0])
+
+ os.rmdir(tempdir)
+
+ return img
+
+
+def register_load_handlers():
+ gimp.register_load_handler('file-openraster-load', 'ora', '')
+ pdb['gimp-register-file-handler-mime']('file-openraster-load', 'image/openraster')
+ pdb['gimp-register-thumbnail-loader']('file-openraster-load', 'file-openraster-load-thumb')
+
+def register_save_handlers():
+ gimp.register_save_handler('file-openraster-save', 'ora', '')
+
+register(
+ 'file-openraster-load-thumb', #name
+ 'loads a thumbnail from an OpenRaster (.ora) file', #description
+ 'loads a thumbnail from an OpenRaster (.ora) file',
+ 'Jon Nordby', #author
+ 'Jon Nordby', #copyright
+ '2009', #year
+ None,
+ None, #image type
+ [ #input args. Format (type, name, description, default [, extra])
+ (PF_STRING, 'filename', 'The name of the file to load', None),
+ (PF_INT, 'thumb-size', 'Preferred thumbnail size', None),
+ ],
+ [ #results. Format (type, name, description)
+ (PF_IMAGE, 'image', 'Thumbnail image'),
+ (PF_INT, 'image-width', 'Width of full-sized image'),
+ (PF_INT, 'image-height', 'Height of full-sized image')
+ ],
+ thumbnail_ora, #callback
+ run_mode_param = False
+)
+
+register(
+ 'file-openraster-save', #name
+ 'save an OpenRaster (.ora) file', #description
+ 'save an OpenRaster (.ora) file',
+ 'Jon Nordby', #author
+ 'Jon Nordby', #copyright
+ '2009', #year
+ 'OpenRaster',
+ '*',
+ [ #input args. Format (type, name, description, default [, extra])
+ (PF_IMAGE, "image", "Input image", None),
+ (PF_DRAWABLE, "drawable", "Input drawable", None),
+ (PF_STRING, "filename", "The name of the file", None),
+ (PF_STRING, "raw-filename", "The name of the file", None),
+ ],
+ [], #results. Format (type, name, description)
+ save_ora, #callback
+ on_query = register_save_handlers,
+ menu = '<Save>'
+)
+
+register(
+ 'file-openraster-load', #name
+ 'load an OpenRaster (.ora) file', #description
+ 'load an OpenRaster (.ora) file',
+ 'Jon Nordby', #author
+ 'Jon Nordby', #copyright
+ '2009', #year
+ 'OpenRaster',
+ None, #image type
+ [ #input args. Format (type, name, description, default [, extra])
+ (PF_STRING, 'filename', 'The name of the file to load', None),
+ (PF_STRING, 'raw-filename', 'The name entered', None),
+ ],
+ [(PF_IMAGE, 'image', 'Output image')], #results. Format (type, name, description)
+ load_ora, #callback
+ on_query = register_load_handlers,
+ menu = "<Load>",
+)
+
+
+main()
diff --git a/plug-ins/pygimp/plug-ins/foggify.py b/plug-ins/pygimp/plug-ins/foggify.py
new file mode 100755
index 0000000..4964d8c
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/foggify.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python2
+
+# Gimp-Python - allows the writing of Gimp plugins in Python.
+# Copyright (C) 1997 James Henstridge <james@daa.com.au>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+from gimpfu import *
+import time
+
+gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
+
+def foggify(img, layer, name, colour, turbulence, opacity):
+
+ gimp.context_push()
+ img.undo_group_start()
+
+ if img.base_type is RGB:
+ type = RGBA_IMAGE
+ else:
+ type = GRAYA_IMAGE
+ fog = gimp.Layer(img, name,
+ layer.width, layer.height, type, opacity, NORMAL_MODE)
+ fog.fill(FILL_TRANSPARENT)
+ img.insert_layer(fog)
+
+ gimp.set_background(colour)
+ pdb.gimp_edit_fill(fog, FILL_BACKGROUND)
+
+ # create a layer mask for the new layer
+ mask = fog.create_mask(0)
+ fog.add_mask(mask)
+
+ # add some clouds to the layer
+ pdb.plug_in_plasma(img, mask, int(time.time()), turbulence)
+
+ # apply the clouds to the layer
+ fog.remove_mask(MASK_APPLY)
+
+ img.undo_group_end()
+ gimp.context_pop()
+
+register(
+ "python-fu-foggify",
+ N_("Add a layer of fog"),
+ "Adds a layer of fog to the image.",
+ "James Henstridge",
+ "James Henstridge",
+ "1999,2007",
+ N_("_Fog..."),
+ "RGB*, GRAY*",
+ [
+ (PF_IMAGE, "image", "Input image", None),
+ (PF_DRAWABLE, "drawable", "Input drawable", None),
+ (PF_STRING, "name", _("_Layer name"), _("Clouds")),
+ (PF_COLOUR, "colour", _("_Fog color"), (240, 180, 70)),
+ (PF_SLIDER, "turbulence", _("_Turbulence"), 1.0, (0, 7, 0.1)),
+ (PF_SLIDER, "opacity", _("Op_acity"), 100, (0, 100, 1)),
+ ],
+ [],
+ foggify,
+ menu="<Image>/Filters/Decor",
+ domain=("gimp20-python", gimp.locale_directory)
+ )
+
+main()
diff --git a/plug-ins/pygimp/plug-ins/gradients-save-as-css.py b/plug-ins/pygimp/plug-ins/gradients-save-as-css.py
new file mode 100755
index 0000000..44179e4
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/gradients-save-as-css.py
@@ -0,0 +1,104 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Allows saving (TODO: and loading) CSS gradient files
+# Copyright (C) 2011 João S. O. Bueno <gwidion@gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+
+# Currently this exports all color segments as RGB linear centered segments.
+# TODO: Respect gradient alpha, off-center segments, different blending
+# functions and HSV colors
+
+from gimpfu import *
+
+gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
+
+w3c_template = """background-image: linear-gradient(top, %s);\n"""
+moz_template = """background-image: -moz-linear-gradient(center top, %s);\n"""
+webkit_template = """background-image: -webkit-gradient(linear, """ \
+ """left top, left bottom, %s);\n"""
+
+color_to_html = lambda c: "rgb(%d,%d,%d)" % tuple(c)[:3]
+
+def format_text(text):
+ counter = 0
+ new_text = []
+ for token in text.split(","):
+ if counter + len(token) > 77:
+ token = "\n " + token
+ counter = 4
+ new_text.append(token)
+ if "\n" in token:
+ counter = len(token.rsplit("\n")[-1]) + 1
+ else:
+ counter += len(token) + 1
+
+ return ",".join(new_text)
+
+def gradient_css_save(gradient, file_name):
+ stops = []
+ wk_stops = []
+ n_segments = pdb.gimp_gradient_get_number_of_segments(gradient)
+ last_stop = None
+ for index in xrange(n_segments):
+ lcolor, lopacity = pdb.gimp_gradient_segment_get_left_color(
+ gradient,
+ index)
+ rcolor, ropacity = pdb.gimp_gradient_segment_get_right_color(
+ gradient,
+ index)
+ lpos = pdb.gimp_gradient_segment_get_left_pos(gradient, index)
+ rpos = pdb.gimp_gradient_segment_get_right_pos(gradient, index)
+
+ lstop = color_to_html(lcolor) + " %d%%" % int(100 * lpos)
+ wk_lstop = "color-stop(%.03f, %s)" %(lpos, color_to_html(lcolor))
+ if lstop != last_stop:
+ stops.append(lstop)
+ wk_stops.append(wk_lstop)
+
+ rstop = color_to_html(rcolor) + " %d%%" % int(100 * rpos)
+ wk_rstop = "color-stop(%.03f, %s)" %(rpos, color_to_html(rcolor))
+
+ stops.append(rstop)
+ wk_stops.append(wk_rstop)
+ last_stop = rstop
+
+ final_text = w3c_template % ", ".join(stops)
+ final_text += moz_template % ",".join(stops)
+ final_text += webkit_template % ",".join(wk_stops)
+
+ with open(file_name, "wt") as file_:
+ file_.write(format_text(final_text))
+
+register(
+ "gradient-save-as-css",
+ "Creates a new palette from a given gradient",
+ "palette_from_gradient (gradient, number, segment_colors) -> None",
+ "Joao S. O. Bueno",
+ "(c) GPL V3.0 or later",
+ "2011",
+ "Save as CSS...",
+ "",
+ [
+ (PF_GRADIENT, "gradient", N_("Gradient to use"),""),
+ (PF_FILENAME, "file_name", N_("File Name"), ""),
+ ],
+ [],
+ gradient_css_save,
+ menu="<Gradients>",
+ domain=("gimp20-python", gimp.locale_directory)
+ )
+main() \ No newline at end of file
diff --git a/plug-ins/pygimp/plug-ins/histogram-export.py b/plug-ins/pygimp/plug-ins/histogram-export.py
new file mode 100755
index 0000000..11da1ba
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/histogram-export.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python2
+#coding: utf-8
+
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+"""
+Exports the image histogram to a text file,
+so that it can be used by other programs
+and loaded into spreadsheets.
+
+The resulting file is a CSV file (Comma Separated
+Values), which can be imported
+directly in most spreadsheet programs.
+
+The first two collums are the bucket boundaries,
+followed by the selected columns. The histogram
+refers to the selected image area, and
+can use either Sample Average data or data
+from the current drawable only.;
+
+The output is in "weighted pixels" - meaning
+all fully transparent pixels are not counted.
+
+Check the gimp-histogram call
+"""
+
+
+from gimpfu import *
+import csv
+import gettext
+
+
+gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
+
+def histogram_export(img, drw, filename,
+ bucket_size, sample_average, output_format):
+ if sample_average:
+ new_img = pdb.gimp_image_duplicate(img)
+ drw = pdb.gimp_image_merge_visible_layers(new_img, CLIP_TO_IMAGE)
+ # TODO: grey images, alpha and non alpha images.
+ channels_txt = ["Value"]
+ channels_gimp = [HISTOGRAM_VALUE]
+ if drw.is_rgb:
+ channels_txt += ["Red", "Green", "Blue"]
+ channels_gimp += [HISTOGRAM_RED, HISTOGRAM_GREEN, HISTOGRAM_BLUE]
+ if drw.has_alpha:
+ channels_txt += ["Alpha"]
+ channels_gimp += [HISTOGRAM_ALPHA]
+ with open(filename, "wt") as hfile:
+ writer = csv.writer(hfile)
+ #headers:
+ writer.writerow(["Range start"] + channels_txt)
+
+ # FIXME: Will need a specialized 'range' for FP color numbers
+ bucket_size = int(bucket_size)
+ for start_range in range(0, 256, bucket_size):
+ row = [start_range]
+ for channel in channels_gimp:
+ result = pdb.gimp_histogram(
+ drw, channel,
+ start_range,
+ min(start_range + bucket_size - 1, 255)
+ )
+ if output_format == "pixel count":
+ count = result[4]
+ else:
+ count = (result[4] / result[3]) if result[3] else 0
+ if output_format == "percent":
+ count = "%.2f%%" % (count * 100)
+ row.append(str(count))
+ writer.writerow(row)
+ if sample_average:
+ pdb.gimp_image_delete(new_img)
+
+register(
+ "histogram-export",
+ N_("Exports the image histogram to a text file (CSV)"),
+ globals()["__doc__"], # This includes the docstring, on the top of the file
+ "João S. O. Bueno",
+ "João S. O. Bueno, 2014",
+ "2014",
+ N_("_Export histogram..."),
+ "*",
+ [(PF_IMAGE, "img", _("_Image"), None),
+ (PF_DRAWABLE, "drw", _("_Drawable"), None),
+ (PF_FILENAME, "filename", _("Histogram _File"), ""),
+ (PF_FLOAT, "bucket_size", _("_Bucket Size"), 1.0),
+ (PF_BOOL, "sample_average", _("Sample _Average"), False),
+ (PF_RADIO, "output_format", _("Output format"), "pixel count",
+ ((_("Pixel count"), "pixel count"),
+ (_("Normalized"), "normalized"),
+ (_("Percent"), "percent"),
+ )
+ )
+ ],
+ [],
+ histogram_export,
+ menu="<Image>/Colors/Info",
+ domain=("gimp20-python", gimp.locale_directory)
+ )
+
+main()
diff --git a/plug-ins/pygimp/plug-ins/palette-offset.py b/plug-ins/pygimp/plug-ins/palette-offset.py
new file mode 100644
index 0000000..8ba0bfe
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/palette-offset.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+from gimpfu import *
+
+gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
+
+def palette_offset(palette, amount):
+ #If palette is read only, work on a copy:
+ editable = pdb.gimp_palette_is_editable(palette)
+ if not editable:palette = pdb.gimp_palette_duplicate (palette)
+
+ num_colors = pdb.gimp_palette_get_info (palette)
+
+ tmp_entry_array = []
+ for i in xrange (num_colors):
+ tmp_entry_array.append ((pdb.gimp_palette_entry_get_name (palette, i),
+ pdb.gimp_palette_entry_get_color (palette, i)))
+ for i in xrange (num_colors):
+ target_index = i + amount
+ if target_index >= num_colors:
+ target_index -= num_colors
+ elif target_index < 0:
+ target_index += num_colors
+ pdb.gimp_palette_entry_set_name (palette, target_index, tmp_entry_array[i][0])
+ pdb.gimp_palette_entry_set_color (palette, target_index, tmp_entry_array[i][1])
+ return palette
+
+
+register(
+ "python-fu-palette-offset",
+ N_("Offset the colors in a palette"),
+ "palette_offset (palette, amount) -> modified_palette",
+ "Joao S. O. Bueno Calligaris, Carol Spears",
+ "(c) Joao S. O. Bueno Calligaris",
+ "2004, 2006",
+ N_("_Offset Palette..."),
+ "",
+ [
+ (PF_PALETTE, "palette", _("Palette"), ""),
+ (PF_INT, "amount", _("Off_set"), 1),
+ ],
+ [(PF_PALETTE, "new-palette", "Result")],
+ palette_offset,
+ menu="<Palettes>",
+ domain=("gimp20-python", gimp.locale_directory)
+ )
+
+main ()
diff --git a/plug-ins/pygimp/plug-ins/palette-sort.py b/plug-ins/pygimp/plug-ins/palette-sort.py
new file mode 100644
index 0000000..c21b8ad
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/palette-sort.py
@@ -0,0 +1,357 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+from gimpfu import *
+# little known, colorsys is part of Python's stdlib
+from colorsys import rgb_to_yiq
+from textwrap import dedent
+from random import randint
+
+gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
+
+AVAILABLE_CHANNELS = (_("Red"), _("Green"), _("Blue"),
+ _("Luma (Y)"),
+ _("Hue"), _("Saturation"), _("Value"),
+ _("Saturation (HSL)"), _("Lightness (HSL)"),
+ _("Index"),
+ _("Random"))
+
+GRAIN_SCALE = (1.0, 1.0 , 1.0,
+ 1.0,
+ 360., 100., 100.,
+ 100., 100.,
+ 16384.,
+ float(0x7ffffff),
+ 100., 256., 256.,
+ 256., 360.,)
+
+SELECT_ALL = 0
+SELECT_SLICE = 1
+SELECT_AUTOSLICE = 2
+SELECT_PARTITIONED = 3
+SELECTIONS = (SELECT_ALL, SELECT_SLICE, SELECT_AUTOSLICE, SELECT_PARTITIONED)
+
+
+def noop(v, i):
+ return v
+
+
+def to_hsv(v, i):
+ return v.to_hsv()
+
+
+def to_hsl(v, i):
+ return v.to_hsl()
+
+
+def to_yiq(v, i):
+ return rgb_to_yiq(*v[:-1])
+
+
+def to_index(v, i):
+ return (i,)
+
+def to_random(v, i):
+ return (randint(0, 0x7fffffff),)
+
+
+channel_getters = [ (noop, 0), (noop, 1), (noop, 2),
+ (to_yiq, 0),
+ (to_hsv, 0), (to_hsv, 1), (to_hsv, 2),
+ (to_hsl, 1), (to_hsl, 2),
+ (to_index, 0),
+ (to_random, 0)]
+
+
+try:
+ from colormath.color_objects import RGBColor, LabColor, LCHabColor
+ AVAILABLE_CHANNELS = AVAILABLE_CHANNELS + (_("Lightness (LAB)"),
+ _("A-color"), _("B-color"),
+ _("Chroma (LCHab)"),
+ _("Hue (LCHab)"))
+ to_lab = lambda v,i: RGBColor(*v[:-1]).convert_to('LAB').get_value_tuple()
+ to_lchab = (lambda v,i:
+ RGBColor(*v[:-1]).convert_to('LCHab').get_value_tuple())
+ channel_getters.extend([(to_lab, 0), (to_lab, 1), (to_lab, 2),
+ (to_lchab, 1), (to_lchab, 2)])
+except ImportError:
+ pass
+
+
+def parse_slice(s, numcolors):
+ """Parse a slice spec and return (start, nrows, length)
+ All items are optional. Omitting them makes the largest possible selection that
+ exactly fits the other items.
+
+ start:nrows,length
+
+
+ '' selects all items, as does ':'
+ ':4,' makes a 4-row selection out of all colors (length auto-determined)
+ ':4' also.
+ ':1,4' selects the first 4 colors
+ ':,4' selects rows of 4 colors (nrows auto-determined)
+ ':4,4' selects 4 rows of 4 colors
+ '4:' selects a single row of all colors after 4, inclusive.
+ '4:,4' selects rows of 4 colors, starting at 4 (nrows auto-determined)
+ '4:4,4' selects 4 rows of 4 colors (16 colors total), beginning at index 4.
+ '4' is illegal (ambiguous)
+
+
+ In general, slices are comparable to a numpy sub-array.
+ 'start at element START, with shape (NROWS, LENGTH)'
+
+ """
+ s = s.strip()
+
+ def notunderstood():
+ raise ValueError('Slice %r not understood. Should be in format'
+ ' START?:NROWS?,ROWLENGTH? eg. "0:4,16".' % s)
+ def _int(v):
+ try:
+ return int(v)
+ except ValueError:
+ notunderstood()
+ if s in ('', ':', ':,'):
+ return 0, 1, numcolors # entire palette, one row
+ if s.count(':') != 1:
+ notunderstood()
+ rowpos = s.find(':')
+ start = 0
+ if rowpos > 0:
+ start = _int(s[:rowpos])
+ numcolors -= start
+ nrows = 1
+ if ',' in s:
+ commapos = s.find(',')
+ nrows = s[rowpos+1:commapos]
+ length = s[commapos+1:]
+ if not nrows:
+ if not length:
+ notunderstood()
+ else:
+ length = _int(length)
+ if length == 0:
+ notunderstood()
+ nrows = numcolors // length
+ if numcolors % length:
+ nrows = -nrows
+ elif not length:
+ nrows = _int(nrows)
+ if nrows == 0:
+ notunderstood()
+ length = numcolors // nrows
+ if numcolors % nrows:
+ length = -length
+ else:
+ nrows = _int(nrows)
+ if nrows == 0:
+ notunderstood()
+ length = _int(length)
+ if length == 0:
+ notunderstood()
+ else:
+ nrows = _int(s[rowpos+1:])
+ if nrows == 0:
+ notunderstood()
+ length = numcolors // nrows
+ if numcolors % nrows:
+ length = -length
+ return start, nrows, length
+
+
+def quantization_grain(channel, g):
+ "Given a channel and a quantization, return the size of a quantization grain"
+ g = max(1.0, g)
+ if g <= 1.0:
+ g = 0.00001
+ else:
+ g = max(0.00001, GRAIN_SCALE[channel] / g)
+ return g
+
+
+def palette_sort(palette, selection, slice_expr, channel1, ascending1,
+ channel2, ascending2, quantize, pchannel, pquantize):
+
+ grain1 = quantization_grain(channel1, quantize)
+ grain2 = quantization_grain(channel2, quantize)
+ pgrain = quantization_grain(pchannel, pquantize)
+
+ #If palette is read only, work on a copy:
+ editable = pdb.gimp_palette_is_editable(palette)
+ if not editable:
+ palette = pdb.gimp_palette_duplicate (palette)
+
+ num_colors = pdb.gimp_palette_get_info (palette)
+
+ start, nrows, length = None, None, None
+ if selection == SELECT_AUTOSLICE:
+ def find_index(color, startindex=0):
+ for i in range(startindex, num_colors):
+ c = pdb.gimp_palette_entry_get_color (palette, i)
+ if c == color:
+ return i
+ return None
+ def hexcolor(c):
+ return "#%02x%02x%02x" % tuple(c[:-1])
+ fg = pdb.gimp_context_get_foreground()
+ bg = pdb.gimp_context_get_background()
+ start = find_index(fg)
+ end = find_index(bg)
+ if start is None:
+ raise ValueError("Couldn't find foreground color %r in palette" % list(fg))
+ if end is None:
+ raise ValueError("Couldn't find background color %r in palette" % list(bg))
+ if find_index(fg, start + 1):
+ raise ValueError('Autoslice cannot be used when more than one'
+ ' instance of an endpoint'
+ ' (%s) is present' % hexcolor(fg))
+ if find_index(bg, end + 1):
+ raise ValueError('Autoslice cannot be used when more than one'
+ ' instance of an endpoint'
+ ' (%s) is present' % hexcolor(bg))
+ if start > end:
+ end, start = start, end
+ length = (end - start) + 1
+ try:
+ _, nrows, _ = parse_slice(slice_expr, length)
+ nrows = abs(nrows)
+ if length % nrows:
+ raise ValueError('Total length %d not evenly divisible'
+ ' by number of rows %d' % (length, nrows))
+ length /= nrows
+ except ValueError:
+ # bad expression is okay here, just assume one row
+ nrows = 1
+ # remaining behaviour is implemented by SELECT_SLICE 'inheritance'.
+ selection= SELECT_SLICE
+ elif selection in (SELECT_SLICE, SELECT_PARTITIONED):
+ start, nrows, length = parse_slice(slice_expr, num_colors)
+
+ channels_getter_1, channel_index = channel_getters[channel1]
+ channels_getter_2, channel2_index = channel_getters[channel2]
+
+ def get_colors(start, end):
+ result = []
+ for i in range(start, end):
+ entry = (pdb.gimp_palette_entry_get_name (palette, i),
+ pdb.gimp_palette_entry_get_color (palette, i))
+ index1 = channels_getter_1(entry[1], i)[channel_index]
+ index2 = channels_getter_2(entry[1], i)[channel2_index]
+ index = ((index1 - (index1 % grain1)) * (1 if ascending1 else -1),
+ (index2 - (index2 % grain2)) * (1 if ascending2 else -1)
+ )
+ result.append((index, entry))
+ return result
+
+ if selection == SELECT_ALL:
+ entry_list = get_colors(0, num_colors)
+ entry_list.sort(key=lambda v:v[0])
+ for i in range(num_colors):
+ pdb.gimp_palette_entry_set_name (palette, i, entry_list[i][1][0])
+ pdb.gimp_palette_entry_set_color (palette, i, entry_list[i][1][1])
+
+ elif selection == SELECT_PARTITIONED:
+ if num_colors < (start + length * nrows) - 1:
+ raise ValueError('Not enough entries in palette to '
+ 'sort complete rows! Got %d, expected >=%d' %
+ (num_colors, start + length * nrows))
+ pchannels_getter, pchannel_index = channel_getters[pchannel]
+ for row in range(nrows):
+ partition_spans = [1]
+ rowstart = start + (row * length)
+ old_color = pdb.gimp_palette_entry_get_color (palette,
+ rowstart)
+ old_partition = pchannels_getter(old_color, rowstart)[pchannel_index]
+ old_partition = old_partition - (old_partition % pgrain)
+ for i in range(rowstart + 1, rowstart + length):
+ this_color = pdb.gimp_palette_entry_get_color (palette, i)
+ this_partition = pchannels_getter(this_color, i)[pchannel_index]
+ this_partition = this_partition - (this_partition % pgrain)
+ if this_partition == old_partition:
+ partition_spans[-1] += 1
+ else:
+ partition_spans.append(1)
+ old_partition = this_partition
+ base = rowstart
+ for size in partition_spans:
+ palette_sort(palette, SELECT_SLICE, '%d:1,%d' % (base, size),
+ channel1, ascending1, channel2, ascending2,
+ quantize, 0, 1.0)
+ base += size
+ else:
+ stride = length
+ if num_colors < (start + stride * nrows) - 1:
+ raise ValueError('Not enough entries in palette to sort '
+ 'complete rows! Got %d, expected >=%d' %
+ (num_colors, start + stride * nrows))
+
+ for row_start in range(start, start + stride * nrows, stride):
+ sublist = get_colors(row_start, row_start + stride)
+ sublist.sort(key=lambda v:v[0])
+ for i, entry in zip(range(row_start, row_start + stride), sublist):
+ pdb.gimp_palette_entry_set_name (palette, i, entry[1][0])
+ pdb.gimp_palette_entry_set_color (palette, i, entry[1][1])
+
+ return palette
+
+register(
+ "python-fu-palette-sort",
+ N_("Sort the colors in a palette"),
+ # FIXME: Write humanly readable help -
+ # (I can't figure out what the plugin does, or how to use the parameters after
+ # David's enhancements even looking at the code -
+ # let alone someone just using GIMP (JS) )
+ dedent("""\
+ palette_sort (palette, selection, slice_expr, channel,
+ channel2, quantize, ascending, pchannel, pquantize) -> new_palette
+ Sorts a palette, or part of a palette, using several options.
+ One can select two color channels over which to sort,
+ and several auxiliary parameters create a 2D sorted
+ palette with sorted rows, among other things.
+ One can optionally install colormath
+ (https://pypi.python.org/pypi/colormath/1.0.8)
+ to GIMP's Python to get even more channels to choose from.
+ """),
+ "João S. O. Bueno, Carol Spears, David Gowers",
+ "João S. O. Bueno, Carol Spears, David Gowers",
+ "2006-2014",
+ N_("_Sort Palette..."),
+ "",
+ [
+ (PF_PALETTE, "palette", _("Palette"), ""),
+ (PF_OPTION, "selections", _("Se_lections"), SELECT_ALL,
+ (_("All"), _("Slice / Array"), _("Autoslice (fg->bg)"),
+ _("Partitioned"))),
+ (PF_STRING, "slice-expr", _("Slice _expression"), ''),
+ (PF_OPTION, "channel1", _("Channel to _sort"), 3,
+ AVAILABLE_CHANNELS),
+ (PF_BOOL, "ascending1", _("_Ascending"), True),
+ (PF_OPTION, "channel2", _("Secondary Channel to s_ort"), 5,
+ AVAILABLE_CHANNELS),
+ (PF_BOOL, "ascending2", _("_Ascending"), True),
+ (PF_FLOAT, "quantize", _("_Quantization"), 0.0),
+ (PF_OPTION, "pchannel", _("_Partitioning channel"), 3,
+ AVAILABLE_CHANNELS),
+ (PF_FLOAT, "pquantize", _("Partition q_uantization"), 0.0),
+ ],
+ [],
+ palette_sort,
+ menu="<Palettes>",
+ domain=("gimp20-python", gimp.locale_directory)
+ )
+
+main ()
diff --git a/plug-ins/pygimp/plug-ins/palette-to-gradient.py b/plug-ins/pygimp/plug-ins/palette-to-gradient.py
new file mode 100644
index 0000000..9b2da49
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/palette-to-gradient.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python2
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+from gimpfu import *
+
+gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
+
+def make_gradient(palette, num_segments, num_colors):
+ gradient = pdb.gimp_gradient_new(palette)
+
+ if (num_segments > 1):
+ pdb.gimp_gradient_segment_range_split_uniform(gradient, 0, -1,
+ num_segments)
+
+ for color_number in range(0,num_segments):
+ if (color_number == num_colors-1):color_number_next = 0
+ else: color_number_next = color_number + 1
+ color_left = pdb.gimp_palette_entry_get_color(palette,
+ color_number)
+ color_right = pdb.gimp_palette_entry_get_color(palette,
+ color_number_next)
+ pdb.gimp_gradient_segment_set_left_color(gradient,
+ color_number, color_left,
+ 100.0)
+ pdb.gimp_gradient_segment_set_right_color(gradient,
+ color_number, color_right,
+ 100.0)
+ pdb.gimp_context_set_gradient(gradient)
+ return gradient
+
+
+def palette_to_gradient_repeating(palette):
+ num_colors = pdb.gimp_palette_get_info(palette)
+ num_segments = num_colors
+ return make_gradient(palette, num_segments, num_colors)
+
+
+register(
+ "python-fu-palette-to-gradient-repeating",
+ N_("Create a repeating gradient using colors from the palette"),
+ "Create a new repeating gradient using colors from the palette.",
+ "Carol Spears, reproduced from previous work by Adrian Likins and Jeff Trefftz",
+ "Carol Spears",
+ "2006",
+ N_("Palette to _Repeating Gradient"),
+ "",
+ [(PF_PALETTE, "palette", _("Palette"), "")],
+ [(PF_GRADIENT, "new-gradient", "Result")],
+ palette_to_gradient_repeating,
+ menu="<Palettes>",
+ domain=("gimp20-python", gimp.locale_directory)
+ )
+
+
+def palette_to_gradient(palette):
+ num_colors = pdb.gimp_palette_get_info(palette)
+ num_segments = num_colors - 1
+ return make_gradient(palette, num_segments, num_colors)
+
+register(
+ "python-fu-palette-to-gradient",
+ N_("Create a gradient using colors from the palette"),
+ "Create a new gradient using colors from the palette.",
+ "Carol Spears, reproduced from previous work by Adrian Likins and Jeff Trefftz",
+ "Carol Spears",
+ "2006",
+ N_("Palette to _Gradient"),
+ "",
+ [(PF_PALETTE, "palette", _("Palette"), "")],
+ [(PF_GRADIENT, "new-gradient", "Result")],
+ palette_to_gradient,
+ menu="<Palettes>",
+ domain=("gimp20-python", gimp.locale_directory)
+ )
+
+main ()
diff --git a/plug-ins/pygimp/plug-ins/py-slice.py b/plug-ins/pygimp/plug-ins/py-slice.py
new file mode 100755
index 0000000..90159aa
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/py-slice.py
@@ -0,0 +1,457 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+#Copyright (c) Manish Singh
+#javascript animation support by Joao S. O. Bueno Calligaris (2004)
+
+# Gimp-Python - allows the writing of Gimp plugins in Python.
+# Copyright (C) 2003, 2005 Manish Singh <yosh@gimp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+# (c) 2003 Manish Singh.
+#"Guillotine implemented ala python, with html output
+# (based on perlotine by Seth Burgess)",
+# Modified by João S. O. Bueno Calligaris to allow dhtml animations (2005)
+
+import os
+
+from gimpfu import *
+import os.path
+
+gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
+
+def pyslice(image, drawable, save_path, html_filename,
+ image_basename, image_extension, separate,
+ image_path, cellspacing, animate, skip_caps):
+
+ cellspacing = int (cellspacing)
+
+ if animate:
+ count = 0
+ drw = []
+ #image.layers is a reversed list of the layers on the image
+ #so, count indexes from number of layers to 0.
+ for i in xrange (len (image.layers) -1, -1, -1):
+ if image.layers[i].visible:
+ drw.append(image.layers[i])
+ count += 1
+ if count == 3:
+ break
+
+
+ vert, horz = get_guides(image)
+
+ if len(vert) == 0 and len(horz) == 0:
+ return
+
+ gimp.progress_init(_("Slice"))
+ progress_increment = 1 / ((len(horz) + 1) * (len(vert) + 1))
+ progress = 0.0
+
+ def check_path(path):
+ path = os.path.abspath(path)
+
+ if not os.path.exists(path):
+ os.mkdir(path)
+
+ return path
+
+ save_path = check_path(save_path)
+
+ if not os.path.isdir(save_path):
+ save_path = os.path.dirname(save_path)
+
+ if separate:
+ image_relative_path = image_path
+ if not image_relative_path.endswith("/"):
+ image_relative_path += "/"
+ image_path = check_path(os.path.join(save_path, image_path))
+ else:
+ image_relative_path = ''
+ image_path = save_path
+
+ tw = TableWriter(os.path.join(save_path, html_filename),
+ cellspacing=cellspacing, animate=animate)
+
+ top = 0
+
+ for i in range(0, len(horz) + 1):
+ if i == len(horz):
+ bottom = image.height
+ else:
+ bottom = image.get_guide_position(horz[i])
+
+ tw.row_start()
+
+ left = 0
+
+ for j in range(0, len(vert) + 1):
+ if j == len(vert):
+ right = image.width
+ else:
+ right = image.get_guide_position(vert[j])
+ if (skip_caps and
+ (
+ (len(horz) >= 2 and (i == 0 or i == len(horz) )) or
+ (len(vert) >= 2 and (j == 0 or j == len(vert) ))
+ )
+ ):
+ skip_stub = True
+ else:
+ skip_stub = False
+
+ if (not animate or skip_stub):
+ src = (image_relative_path +
+ slice (image, None, image_path,
+ image_basename, image_extension,
+ left, right, top, bottom, i, j, ""))
+ else:
+ src = []
+ for layer, postfix in zip (drw, ("", "hover", "clicked")):
+ src.append (image_relative_path +
+ slice(image, layer, image_path,
+ image_basename, image_extension,
+ left, right, top, bottom, i, j, postfix))
+
+ tw.cell(src, right - left, bottom - top, i, j, skip_stub)
+
+ left = right + cellspacing
+
+ progress += progress_increment
+ gimp.progress_update(progress)
+
+ tw.row_end()
+
+ top = bottom + cellspacing
+
+ tw.close()
+
+def slice(image, drawable, image_path, image_basename, image_extension,
+ left, right, top, bottom, i, j, postfix):
+ if postfix:
+ postfix = "_" + postfix
+ src = "%s_%d_%d%s.%s" % (image_basename, i, j, postfix, image_extension)
+ filename = os.path.join(image_path, src)
+
+ if not drawable:
+ temp_image = image.duplicate()
+ temp_drawable = temp_image.active_layer
+ else:
+ if image.base_type == INDEXED:
+ #gimp_layer_new_from_drawable doesn't work for indexed images.
+ #(no colormap on new images)
+ original_active = image.active_layer
+ image.active_layer = drawable
+ temp_image = image.duplicate()
+ temp_drawable = temp_image.active_layer
+ image.active_layer = original_active
+ temp_image.disable_undo()
+ #remove all layers but the intended one
+ while len (temp_image.layers) > 1:
+ if temp_image.layers[0] != temp_drawable:
+ pdb.gimp_image_remove_layer (temp_image, temp_image.layers[0])
+ else:
+ pdb.gimp_image_remove_layer (temp_image, temp_image.layers[1])
+ else:
+ temp_image = pdb.gimp_image_new (drawable.width, drawable.height,
+ image.base_type)
+ temp_drawable = pdb.gimp_layer_new_from_drawable (drawable, temp_image)
+ temp_image.insert_layer (temp_drawable)
+
+ temp_image.disable_undo()
+ temp_image.crop(right - left, bottom - top, left, top)
+ if image_extension == "gif" and image.base_type == RGB:
+ pdb.gimp_image_convert_indexed (temp_image, CONVERT_DITHER_NONE,
+ CONVERT_PALETTE_GENERATE, 255,
+ True, False, False)
+ if image_extension == "jpg" and image.base_type == INDEXED:
+ pdb.gimp_image_convert_rgb (temp_image)
+
+ pdb.gimp_file_save(temp_image, temp_drawable, filename, filename)
+
+ gimp.delete(temp_image)
+ return src
+
+class GuideIter:
+ def __init__(self, image):
+ self.image = image
+ self.guide = 0
+
+ def __iter__(self):
+ return iter(self.next_guide, 0)
+
+ def next_guide(self):
+ self.guide = self.image.find_next_guide(self.guide)
+ return self.guide
+
+def get_guides(image):
+ vguides = []
+ hguides = []
+
+ for guide in GuideIter(image):
+ orientation = image.get_guide_orientation(guide)
+
+ guide_position = image.get_guide_position(guide)
+
+ if guide_position > 0:
+ if orientation == ORIENTATION_VERTICAL:
+ if guide_position < image.width:
+ vguides.append((guide_position, guide))
+ elif orientation == ORIENTATION_HORIZONTAL:
+ if guide_position < image.height:
+ hguides.append((guide_position, guide))
+
+ def position_sort(x, y):
+ return cmp(x[0], y[0])
+
+ vguides.sort(position_sort)
+ hguides.sort(position_sort)
+
+ vguides = [g[1] for g in vguides]
+ hguides = [g[1] for g in hguides]
+
+ return vguides, hguides
+
+class TableWriter:
+ def __init__(self, filename, cellpadding=0, cellspacing=0, border=0,
+ animate=False):
+
+ self.filename = filename
+ self.table_attrs = {}
+
+ #Hellraisen IE 6 doesn't support CSS for table control.
+ self.table_attrs['cellpadding'] = cellpadding
+ self.table_attrs['cellspacing'] = cellspacing
+ self.table_attrs['border'] = border
+
+ self.image_prefix = os.path.basename (filename)
+ self.image_prefix = self.image_prefix.split(".")[0]
+ self.image_prefix = self.image_prefix.replace ("-", "_")
+ self.image_prefix = self.image_prefix.replace (" ", "_")
+
+
+ if animate:
+ self.animate = True
+ self.images = []
+ else:
+ self.animate = False
+
+ if os.path.exists (filename):
+ #The plug-in is running to overwrite a previous
+ #version of the file. This will parse the href targets already
+ #in the file to preserve them.
+ self.urls = self.parse_urls ()
+ else:
+ self.urls = []
+
+ self.url_index = 0
+
+ self.html = open(filename, 'wt')
+ self.open()
+
+ def next_url (self):
+ if self.url_index < len (self.urls):
+ self.url_index += 1
+ return self.urls [self.url_index - 1]
+ else:
+ #Default url to use in the anchor tags:
+ return ("#")
+
+ def write(self, s, vals=None):
+ if vals:
+ s = s % vals
+
+ self.html.write(s + '\n')
+
+ def open(self):
+ out = '''<!--HTML SNIPPET GENERATED BY GIMP
+
+WARNING!! This is NOT a fully valid HTML document, it is rather a piece of
+HTML generated by GIMP's py-slice plugin that should be embedded in an HTML
+or XHTML document to be valid.
+
+Replace the href targets in the anchor (<a >) for your URLS to have it working
+as a menu.
+ -->\n'''
+ out += '<table'
+
+ for attr, value in self.table_attrs.iteritems():
+ out += ' %s="%s"' % (attr, value)
+
+ out += '>'
+
+ self.write(out)
+
+ def close(self):
+ self.write('</table>\n')
+ prefix = self.image_prefix
+ if self.animate:
+ out = """
+<script language="javascript" type="text/javascript">
+/* Made with GIMP */
+
+/* Preload images: */
+ images_%s = new Array();
+ \n""" % prefix
+ for image in self.images:
+ for type_ in ("plain", "hover", "clicked"):
+ if image.has_key(type_):
+ image_index = ("%d_%d_%s" %
+ (image["index"][0],
+ image["index"][1], type_))
+ out += (" images_%s[\"%s\"] = new Image();\n" %
+ (prefix, image_index))
+ out += (" images_%s[\"%s\"].src = \"%s\";\n" %
+ (prefix, image_index, image[type_]))
+
+ out+= """
+function exchange (image, images_array_name, event)
+ {
+ name = image.name;
+ images = eval (images_array_name);
+
+ switch (event)
+ {
+ case 0:
+ image.src = images[name + "_plain"].src;
+ break;
+ case 1:
+ image.src = images[name + "_hover"].src;
+ break;
+ case 2:
+ image.src = images[name + "_clicked"].src;
+ break;
+ case 3:
+ image.src = images[name + "_hover"].src;
+ break;
+ }
+
+ }
+</script>
+<!--
+End of the part generated by GIMP
+-->
+"""
+ self.write (out)
+
+
+ def row_start(self):
+ self.write(' <tr>')
+
+ def row_end(self):
+ self.write('</tr>\n')
+
+ def cell(self, src, width, height, row=0, col=0, skip_stub = False):
+ if isinstance (src, list):
+ prefix = "images_%s" % self.image_prefix
+ self.images.append ({"index" : (row, col), "plain" : src[0]})
+
+ out = (' <td><a href="%s"><img alt="" src="%s" ' +
+ 'style="width: %dpx; height: %dpx; border-width: 0px" \n') %\
+ (self.next_url(), src[0], width, height)
+ out += 'name="%d_%d" \n' % (row, col)
+ if len(src) >= 2:
+ self.images[-1]["hover"] = src [1]
+ out += """ onmouseout="exchange(this, '%s', 0);"\n""" % \
+ prefix
+ out += """ onmouseover="exchange(this, '%s', 1);"\n""" % \
+ prefix
+ if len(src) >= 3:
+ self.images[-1]["clicked"] = src [2]
+ out += """ onmousedown="exchange(this, '%s', 2);"\n""" % \
+ prefix
+ out += """ onmouseup="exchange(this, '%s', 3);"\n""" % \
+ prefix
+
+
+
+ out += "/></a></td>\n"
+
+ else:
+ if skip_stub:
+ out = (' <td><img alt=" " src="%s" style="width: %dpx; ' +
+ ' height: %dpx; border-width: 0px;"></td>') % \
+ (src, width, height)
+ else:
+ out = (' <td><a href="#"><img alt=" " src="%s" ' +
+ ' style="width: %dpx; height: %dpx; border-width: 0px;">' +
+ '</a></td>') % (src, width, height)
+ self.write(out)
+ def parse_urls (self):
+ """
+ This will parse any url targets in the href="XX" fields
+ of the given file and return then as a list
+ """
+ import re
+ url_list = []
+ try:
+ html_file = open (self.filename)
+
+ # Regular expression to pick everything up to the next
+ # doublequote character after finding the sequence 'href="'.
+ # The found sequences will be returned as a list by the
+ # "findall" method.
+ expr = re.compile (r"""href\=\"([^\"]*?)\"""")
+ url_list = expr.findall (html_file.read (2 ** 18))
+ html_file.close()
+
+ except:
+ # silently ignore any errors parsing this. The file being
+ # overwritten may not be a file created by py-slice.
+ pass
+
+ return url_list
+
+
+register(
+ "python-fu-slice",
+ # table snippet means a small piece of HTML code here
+ N_("Cuts an image along its guides, creates images and a HTML table snippet"),
+ """Add guides to an image. Then run this. It will cut along the guides,
+ and give you the html to reassemble the resulting images. If you
+ choose to generate javascript for onmouseover and clicked events, it
+ will use the lower three visible layers on the image for normal,
+ onmouseover and clicked states, in that order. If skip caps is
+ enabled, table cells on the edge of the table won't become animated,
+ and its images will be taken from the active layer.""",
+ "Manish Singh",
+ "Manish Singh",
+ "2003",
+ _("_Slice..."),
+ "*",
+ [
+ (PF_IMAGE, "image", "Input image", None),
+ (PF_DRAWABLE, "drawable", "Input drawable", None),
+ (PF_DIRNAME, "save-path", _("Path for HTML export"), os.getcwd()),
+ (PF_STRING, "html-filename", _("Filename for export"), "slice.html"),
+ (PF_STRING, "image-basename", _("Image name prefix"), "slice"),
+ (PF_RADIO, "image-extension", _("Image format"), "gif", (("gif", "gif"), ("jpg", "jpg"), ("png", "png"))),
+ (PF_TOGGLE, "separate-image-dir", _("Separate image folder"),
+ False),
+ (PF_STRING, "relative-image-path", _("Folder for image export"), "images"),
+ (PF_SPINNER, "cellspacing", _("Space between table elements"), 0,
+ (0,15,1)),
+ (PF_TOGGLE, "animate", _("Javascript for onmouseover and clicked"),
+ False),
+ # table caps are table cells on the edge of the table
+ (PF_TOGGLE, "skip-caps", _("Skip animation for table caps"), True)
+ ],
+ [],
+ pyslice,
+ menu="<Image>/Filters/Web",
+ domain=("gimp20-python", gimp.locale_directory)
+ )
+
+main()
diff --git a/plug-ins/pygimp/plug-ins/pyconsole.py b/plug-ins/pygimp/plug-ins/pyconsole.py
new file mode 100644
index 0000000..ee096d5
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/pyconsole.py
@@ -0,0 +1,749 @@
+#
+# pyconsole.py
+#
+# Copyright (C) 2004-2006 by Yevgen Muntyan <muntyan@math.tamu.edu>
+# Portions of code by Geoffrey French.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public version 2.1 as
+# published by the Free Software Foundation.
+#
+# See COPYING.lib file that comes with this distribution for full text
+# of the license.
+#
+
+# This module 'runs' python interpreter in a TextView widget.
+# The main class is Console, usage is:
+# Console(locals=None, banner=None, completer=None, use_rlcompleter=True, start_script='') -
+# it creates the widget and 'starts' interactive session; see the end
+# of this file. If start_script is not empty, it pastes it as it was
+# entered from keyboard.
+#
+# Console has "command" signal which is emitted when code is about to
+# be executed. You may connect to it using console.connect or
+# console.connect_after to get your callback ran before or after the
+# code is executed.
+#
+# To modify output appearance, set attributes of console.stdout_tag and
+# console.stderr_tag.
+#
+# Console may subclass a type other than gtk.TextView, to allow syntax
+# highlighting and stuff,
+# e.g.:
+# console_type = pyconsole.ConsoleType(moo.edit.TextView)
+# console = console_type(use_rlcompleter=False, start_script="import moo\nimport gtk\n")
+#
+# This widget is not a replacement for real terminal with python running
+# inside: GtkTextView is not a terminal.
+# The use case is: you have a python program, you create this widget,
+# and inspect your program interiors.
+
+import gtk
+import gtk.gdk as gdk
+import gobject
+import pango
+import gtk.keysyms as _keys
+import code
+import sys
+import keyword
+import re
+
+# commonprefix() from posixpath
+def _commonprefix(m):
+ "Given a list of pathnames, returns the longest common leading component"
+ if not m: return ''
+ prefix = m[0]
+ for item in m:
+ for i in range(len(prefix)):
+ if prefix[:i+1] != item[:i+1]:
+ prefix = prefix[:i]
+ if i == 0:
+ return ''
+ break
+ return prefix
+
+class _ReadLine(object):
+
+ class Output(object):
+ def __init__(self, console, tag_name):
+ object.__init__(self)
+ self.buffer = console.get_buffer()
+ self.tag_name = tag_name
+ def write(self, text):
+ pos = self.buffer.get_iter_at_mark(self.buffer.get_insert())
+ self.buffer.insert_with_tags_by_name(pos, text, self.tag_name)
+
+ class History(object):
+ def __init__(self):
+ object.__init__(self)
+ self.items = ['']
+ self.ptr = 0
+ self.edited = {}
+
+ def commit(self, text):
+ if text and self.items[-1] != text:
+ self.items.append(text)
+ self.ptr = 0
+ self.edited = {}
+
+ def get(self, dir, text):
+ if len(self.items) == 1:
+ return None
+
+ if text != self.items[self.ptr]:
+ self.edited[self.ptr] = text
+ elif self.edited.has_key(self.ptr):
+ del self.edited[self.ptr]
+
+ self.ptr = self.ptr + dir
+ if self.ptr >= len(self.items):
+ self.ptr = 0
+ elif self.ptr < 0:
+ self.ptr = len(self.items) - 1
+
+ try:
+ return self.edited[self.ptr]
+ except KeyError:
+ return self.items[self.ptr]
+
+ def __init__(self, quit_func=None):
+ object.__init__(self)
+
+ self.quit_func = quit_func
+
+ self.set_wrap_mode(gtk.WRAP_CHAR)
+ self.modify_font(pango.FontDescription("Monospace"))
+
+ self.buffer = self.get_buffer()
+ self.buffer.connect("insert-text", self.on_buf_insert)
+ self.buffer.connect("delete-range", self.on_buf_delete)
+ self.buffer.connect("mark-set", self.on_buf_mark_set)
+ self.do_insert = False
+ self.do_delete = False
+
+ self.stdout_tag = self.buffer.create_tag("stdout", foreground="#006000")
+ self.stderr_tag = self.buffer.create_tag("stderr", foreground="#B00000")
+ self._stdout = _ReadLine.Output(self, "stdout")
+ self._stderr = _ReadLine.Output(self, "stderr")
+
+ self.cursor = self.buffer.create_mark("cursor",
+ self.buffer.get_start_iter(),
+ False)
+ insert = self.buffer.get_insert()
+ self.cursor.set_visible(True)
+ insert.set_visible(False)
+
+ self.ps = ''
+ self.in_raw_input = False
+ self.in_modal_raw_input = False
+ self.run_on_raw_input = None
+ self.tab_pressed = 0
+ self.history = _ReadLine.History()
+ self.nonword_re = re.compile("[^\w\._]")
+
+ def freeze_undo(self):
+ try: self.begin_not_undoable_action()
+ except: pass
+
+ def thaw_undo(self):
+ try: self.end_not_undoable_action()
+ except: pass
+
+ def raw_input(self, ps=None):
+ '''Show prompt 'ps' and enter input mode until the current input
+ is committed.'''
+
+ if ps:
+ self.ps = ps
+ else:
+ self.ps = ''
+
+ iter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
+
+ if ps:
+ self.freeze_undo()
+ self.buffer.insert(iter, self.ps)
+ self.thaw_undo()
+
+ self.__move_cursor_to(iter)
+ self.scroll_to_mark(self.cursor, 0.2)
+
+ self.in_raw_input = True
+
+ if self.run_on_raw_input:
+ run_now = self.run_on_raw_input
+ self.run_on_raw_input = None
+ self.buffer.insert_at_cursor(run_now + '\n')
+
+ def modal_raw_input(self, text):
+ '''Starts raw input in modal mode. The event loop is spinned until
+ the input is committed. Returns the text entered after the prompt.'''
+ orig_ps = self.ps
+
+ self.raw_input(text)
+ self.in_modal_raw_input = True
+
+ while self.in_modal_raw_input:
+ gtk.main_iteration()
+
+ self.ps = orig_ps
+ self.in_modal_raw_input = False
+ self.in_raw_input = False
+
+ return self.modal_raw_input_result
+
+ def modal_input(self, text):
+ return eval(self.modal_raw_input(text))
+
+ # Each time the insert mark is modified, move the cursor to it.
+ def on_buf_mark_set(self, buffer, iter, mark):
+ if mark is not buffer.get_insert():
+ return
+ start = self.__get_start()
+ end = self.__get_end()
+ if iter.compare(self.__get_start()) >= 0 and \
+ iter.compare(self.__get_end()) <= 0:
+ buffer.move_mark_by_name("cursor", iter)
+ self.scroll_to_mark(self.cursor, 0.2)
+
+ def __insert(self, iter, text):
+ self.do_insert = True
+ self.buffer.insert(iter, text)
+ self.do_insert = False
+
+ # Make sure that text insertions while in text input mode are properly
+ # committed to the history.
+ def on_buf_insert(self, buf, iter, text, len):
+ # Bail out if not in input mode.
+ if not self.in_raw_input or self.do_insert or not len:
+ return
+
+ buf.stop_emission("insert-text")
+ lines = text.splitlines()
+ need_eol = False
+ for l in lines:
+ if need_eol:
+ self._commit()
+ iter = self.__get_cursor()
+ else:
+ cursor = self.__get_cursor()
+ if iter.compare(self.__get_start()) < 0:
+ iter = cursor
+ elif iter.compare(self.__get_end()) > 0:
+ iter = cursor
+ else:
+ self.__move_cursor_to(iter)
+ need_eol = True
+ self.__insert(iter, l)
+ self.__move_cursor(0)
+
+ def __delete(self, start, end):
+ self.do_delete = True
+ self.buffer.delete(start, end)
+ self.do_delete = False
+
+ def on_buf_delete(self, buf, start, end):
+ if not self.in_raw_input or self.do_delete:
+ return
+
+ buf.stop_emission("delete-range")
+
+ start.order(end)
+ line_start = self.__get_start()
+ line_end = self.__get_end()
+
+ if start.compare(line_end) > 0:
+ return
+ if end.compare(line_start) < 0:
+ return
+
+ self.__move_cursor(0)
+
+ if start.compare(line_start) < 0:
+ start = line_start
+ if end.compare(line_end) > 0:
+ end = line_end
+ self.__delete(start, end)
+
+ # We overload the key press event handler to handle "special keys"
+ # when in input mode to make history browsing, completions, etc. work.
+ def do_key_press_event(self, event, parent_type):
+ if not self.in_raw_input:
+ return parent_type.do_key_press_event(self, event)
+
+ tab_pressed = self.tab_pressed
+ self.tab_pressed = 0
+ handled = True
+
+ state = event.state & (gdk.SHIFT_MASK |
+ gdk.CONTROL_MASK |
+ gdk.MOD1_MASK)
+ keyval = event.keyval
+
+ if not state:
+ if keyval == _keys.Escape:
+ pass
+ elif keyval == _keys.Return:
+ self._commit()
+ elif keyval == _keys.Up:
+ self.__history(-1)
+ elif keyval == _keys.Down:
+ self.__history(1)
+ elif keyval == _keys.Left:
+ self.__move_cursor(-1)
+ elif keyval == _keys.Right:
+ self.__move_cursor(1)
+ elif keyval == _keys.Home:
+ self.__move_cursor(-10000)
+ elif keyval == _keys.End:
+ self.__move_cursor(10000)
+ elif keyval == _keys.Tab:
+ cursor = self.__get_cursor()
+ if cursor.starts_line():
+ handled = False
+ else:
+ cursor.backward_char()
+ if cursor.get_char().isspace():
+ handled = False
+ else:
+ self.tab_pressed = tab_pressed + 1
+ self.__complete()
+ else:
+ handled = False
+ elif state == gdk.CONTROL_MASK:
+ if keyval == _keys.u:
+ start = self.__get_start()
+ end = self.__get_cursor()
+ self.__delete(start, end)
+ elif keyval == _keys.d:
+ if self.quit_func:
+ self.quit_func()
+ else:
+ handled = False
+ else:
+ handled = False
+
+ # Handle ordinary keys
+ if not handled:
+ return parent_type.do_key_press_event(self, event)
+ else:
+ return True
+
+ def __history(self, dir):
+ text = self._get_line()
+ new_text = self.history.get(dir, text)
+ if not new_text is None:
+ self.__replace_line(new_text)
+ self.__move_cursor(0)
+ self.scroll_to_mark(self.cursor, 0.2)
+
+ def __get_cursor(self):
+ '''Returns an iterator at the current cursor position.'''
+ return self.buffer.get_iter_at_mark(self.cursor)
+
+ def __get_start(self):
+ '''Returns an iterator at the start of the input on the current
+ cursor line.'''
+
+ iter = self.__get_cursor()
+ iter.set_line(iter.get_line())
+ iter.forward_chars(len(self.ps))
+ return iter
+
+ def __get_end(self):
+ '''Returns an iterator at the end of the cursor line.'''
+ iter = self.__get_cursor()
+ if not iter.ends_line():
+ iter.forward_to_line_end()
+ return iter
+
+ def __get_text(self, start, end):
+ '''Get text between 'start' and 'end' markers.'''
+ return self.buffer.get_text(start, end, False)
+
+ def __move_cursor_to(self, iter):
+ self.buffer.place_cursor(iter)
+ self.buffer.move_mark_by_name("cursor", iter)
+
+ def __move_cursor(self, howmany):
+ iter = self.__get_cursor()
+ end = self.__get_cursor()
+ if not end.ends_line():
+ end.forward_to_line_end()
+ line_len = end.get_line_offset()
+ move_to = iter.get_line_offset() + howmany
+ move_to = min(max(move_to, len(self.ps)), line_len)
+ iter.set_line_offset(move_to)
+ self.__move_cursor_to(iter)
+
+ def __delete_at_cursor(self, howmany):
+ iter = self.__get_cursor()
+ end = self.__get_cursor()
+ if not end.ends_line():
+ end.forward_to_line_end()
+ line_len = end.get_line_offset()
+ erase_to = iter.get_line_offset() + howmany
+ if erase_to > line_len:
+ erase_to = line_len
+ elif erase_to < len(self.ps):
+ erase_to = len(self.ps)
+ end.set_line_offset(erase_to)
+ self.__delete(iter, end)
+
+ def __get_width(self):
+ '''Estimate the number of characters that will fit in the area
+ currently allocated to this widget.'''
+
+ if not (self.flags() & gtk.REALIZED):
+ return 80
+
+ context = self.get_pango_context()
+ metrics = context.get_metrics(context.get_font_description(),
+ context.get_language())
+ pix_width = metrics.get_approximate_char_width()
+ return self.allocation.width * pango.SCALE / pix_width
+
+ def __print_completions(self, completions):
+ line_start = self.__get_text(self.__get_start(), self.__get_cursor())
+ line_end = self.__get_text(self.__get_cursor(), self.__get_end())
+ iter = self.buffer.get_end_iter()
+ self.__move_cursor_to(iter)
+ self.__insert(iter, "\n")
+
+ width = max(self.__get_width(), 4)
+ max_width = max([len(s) for s in completions])
+ n_columns = max(int(width / (max_width + 1)), 1)
+ col_width = int(width / n_columns)
+ total = len(completions)
+ col_length = total / n_columns
+ if total % n_columns:
+ col_length = col_length + 1
+ col_length = max(col_length, 1)
+
+ if col_length == 1:
+ n_columns = total
+ col_width = width / total
+
+ for i in range(col_length):
+ for j in range(n_columns):
+ ind = i + j*col_length
+ if ind < total:
+ if j == n_columns - 1:
+ n_spaces = 0
+ else:
+ n_spaces = col_width - len(completions[ind])
+ self.__insert(iter, completions[ind] + " " * n_spaces)
+ self.__insert(iter, "\n")
+
+ self.__insert(iter, "%s%s%s" % (self.ps, line_start, line_end))
+ iter.set_line_offset(len(self.ps) + len(line_start))
+ self.__move_cursor_to(iter)
+ self.scroll_to_mark(self.cursor, 0.2)
+
+ def __complete(self):
+ text = self.__get_text(self.__get_start(), self.__get_cursor())
+ start = ''
+ word = text
+ nonwords = self.nonword_re.findall(text)
+ if nonwords:
+ last = text.rfind(nonwords[-1]) + len(nonwords[-1])
+ start = text[:last]
+ word = text[last:]
+
+ completions = self.complete(word)
+
+ if completions:
+ prefix = _commonprefix(completions)
+ if prefix != word:
+ start_iter = self.__get_start()
+ start_iter.forward_chars(len(start))
+ end_iter = start_iter.copy()
+ end_iter.forward_chars(len(word))
+ self.__delete(start_iter, end_iter)
+ self.__insert(end_iter, prefix)
+ elif self.tab_pressed > 1:
+ self.freeze_undo()
+ self.__print_completions(completions)
+ self.thaw_undo()
+ self.tab_pressed = 0
+
+ def complete(self, text):
+ return None
+
+ def _get_line(self):
+ '''Return the current input behind the prompt.'''
+ start = self.__get_start()
+ end = self.__get_end()
+ return self.buffer.get_text(start, end, False)
+
+ def __replace_line(self, new_text):
+ '''Replace the current input with 'new_text' '''
+ start = self.__get_start()
+ end = self.__get_end()
+ self.__delete(start, end)
+ self.__insert(end, new_text)
+
+ def _commit(self):
+ '''Commit the input entered on the current line.'''
+
+ # Find iterator and end of cursor line.
+ end = self.__get_cursor()
+ if not end.ends_line():
+ end.forward_to_line_end()
+
+ # Get text at current line.
+ text = self._get_line()
+
+ # Move cursor to the end of the line, insert new line.
+ self.__move_cursor_to(end)
+ self.freeze_undo()
+ self.__insert(end, "\n")
+
+ self.history.commit(text)
+ if self.in_modal_raw_input:
+ self.in_modal_raw_input = False
+ self.modal_raw_input_result = text
+ else:
+ self.in_raw_input = False
+ self.do_raw_input(text)
+
+ self.thaw_undo()
+
+ def do_raw_input(self, text):
+ pass
+
+
+class _Console(_ReadLine, code.InteractiveInterpreter):
+ def __init__(self, locals=None, banner=None,
+ completer=None, use_rlcompleter=True,
+ start_script=None, quit_func=None):
+ _ReadLine.__init__(self, quit_func)
+
+ code.InteractiveInterpreter.__init__(self, locals)
+ self.locals["__console__"] = self
+
+ # The builtin raw_input function reads from stdin, we don't want
+ # this. Therefore, replace this function with our own modal raw
+ # input function.
+ exec "import __builtin__" in self.locals
+ self.locals['__builtin__'].__dict__['raw_input'] = lambda text='': self.modal_raw_input(text)
+ self.locals['__builtin__'].__dict__['input'] = lambda text='': self.modal_input(text)
+
+ self.start_script = start_script
+ self.completer = completer
+ self.banner = banner
+
+ if not self.completer and use_rlcompleter:
+ try:
+ import rlcompleter
+ self.completer = rlcompleter.Completer()
+ except ImportError:
+ pass
+
+ self.ps1 = ">>> "
+ self.ps2 = "... "
+ self.__start()
+ self.run_on_raw_input = start_script
+ self.raw_input(self.ps1)
+
+ def __start(self):
+ self.cmd_buffer = ""
+
+ self.freeze_undo()
+ self.thaw_undo()
+
+ self.do_delete = True
+ self.buffer.set_text("")
+ self.do_delete = False
+
+ if self.banner:
+ iter = self.buffer.get_start_iter()
+ self.buffer.insert_with_tags_by_name(iter, self.banner, "stdout")
+ if not iter.starts_line():
+ self.buffer.insert(iter, "\n")
+
+ def clear(self, start_script=None):
+ if start_script is None:
+ start_script = self.start_script
+ else:
+ self.start_script = start_script
+
+ self.__start()
+ self.run_on_raw_input = start_script
+ self.raw_input(self.ps1)
+
+ def do_raw_input(self, text):
+ if self.cmd_buffer:
+ cmd = self.cmd_buffer + "\n" + text
+ else:
+ cmd = text
+
+ saved_stdout, saved_stderr = sys.stdout, sys.stderr
+ sys.stdout, sys.stderr = self._stdout, self._stderr
+
+ if self.runsource(cmd):
+ self.cmd_buffer = cmd
+ ps = self.ps2
+ else:
+ self.cmd_buffer = ''
+ ps = self.ps1
+
+ sys.stdout, sys.stderr = saved_stdout, saved_stderr
+ self.raw_input(ps)
+
+ def do_command(self, code):
+ try:
+ eval(code, self.locals)
+ except SystemExit:
+ if self.quit_func:
+ self.quit_func()
+ else:
+ raise
+ except:
+ self.showtraceback()
+
+ def runcode(self, code):
+ if gtk.pygtk_version[1] < 8:
+ self.do_command(code)
+ else:
+ self.emit("command", code)
+
+ def complete_attr(self, start, end):
+ try:
+ obj = eval(start, self.locals)
+ strings = dir(obj)
+
+ if end:
+ completions = {}
+ for s in strings:
+ if s.startswith(end):
+ completions[s] = None
+ completions = completions.keys()
+ else:
+ completions = strings
+
+ completions.sort()
+ return [start + "." + s for s in completions]
+ except:
+ return None
+
+ def complete(self, text):
+ if self.completer:
+ completions = []
+ i = 0
+ try:
+ while 1:
+ s = self.completer.complete(text, i)
+ if s:
+ completions.append(s)
+ i = i + 1
+ else:
+ completions.sort()
+ return completions
+ except NameError:
+ return None
+
+ dot = text.rfind(".")
+ if dot >= 0:
+ return self.complete_attr(text[:dot], text[dot+1:])
+
+ completions = {}
+ strings = keyword.kwlist
+
+ if self.locals:
+ strings.extend(self.locals.keys())
+
+ try: strings.extend(eval("globals()", self.locals).keys())
+ except: pass
+
+ try:
+ exec "import __builtin__" in self.locals
+ strings.extend(eval("dir(__builtin__)", self.locals))
+ except:
+ pass
+
+ for s in strings:
+ if s.startswith(text):
+ completions[s] = None
+ completions = completions.keys()
+ completions.sort()
+ return completions
+
+
+def ReadLineType(t=gtk.TextView):
+ class readline(t, _ReadLine):
+ def __init__(self, *args, **kwargs):
+ t.__init__(self)
+ _ReadLine.__init__(self, *args, **kwargs)
+ def do_key_press_event(self, event):
+ return _ReadLine.do_key_press_event(self, event, t)
+ gobject.type_register(readline)
+ return readline
+
+def ConsoleType(t=gtk.TextView):
+ class console_type(t, _Console):
+ __gsignals__ = {
+ 'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (object,)),
+ 'key-press-event' : 'override'
+ }
+
+ def __init__(self, *args, **kwargs):
+ if gtk.pygtk_version[1] < 8:
+ gobject.GObject.__init__(self)
+ else:
+ t.__init__(self)
+ _Console.__init__(self, *args, **kwargs)
+
+ def do_command(self, code):
+ return _Console.do_command(self, code)
+
+ def do_key_press_event(self, event):
+ return _Console.do_key_press_event(self, event, t)
+
+ def get_default_size(self):
+ context = self.get_pango_context()
+ metrics = context.get_metrics(context.get_font_description(),
+ context.get_language())
+ width = metrics.get_approximate_char_width()
+ height = metrics.get_ascent() + metrics.get_descent()
+
+ # Default to a 80x40 console
+ width = pango.PIXELS(int(width * 80 * 1.05))
+ height = pango.PIXELS(height * 40)
+
+ return width, height
+
+ if gtk.pygtk_version[1] < 8:
+ gobject.type_register(console_type)
+
+ return console_type
+
+ReadLine = ReadLineType()
+Console = ConsoleType()
+
+def _make_window():
+ window = gtk.Window()
+ window.set_title("pyconsole.py")
+ swin = gtk.ScrolledWindow()
+ swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
+ window.add(swin)
+ console = Console(banner="Hello there!",
+ use_rlcompleter=False,
+ start_script="from gtk import *\n")
+ swin.add(console)
+
+ width, height = console.get_default_size()
+ sb_width, sb_height = swin.get_vscrollbar().size_request()
+
+ window.set_default_size(width + sb_width, height)
+ window.show_all()
+
+ if not gtk.main_level():
+ window.connect("destroy", gtk.main_quit)
+ gtk.main()
+
+ return console
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2 or sys.argv[1] != '-gimp':
+ _make_window()
diff --git a/plug-ins/pygimp/plug-ins/python-console.py b/plug-ins/pygimp/plug-ins/python-console.py
new file mode 100755
index 0000000..a8c7e4c
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/python-console.py
@@ -0,0 +1,245 @@
+#!/usr/bin/env python2
+
+# Gimp-Python - allows the writing of Gimp plugins in Python.
+# Copyright (C) 1997 James Henstridge <james@daa.com.au>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+from gimpfu import *
+
+t = gettext.translation('gimp20-python', gimp.locale_directory, fallback=True)
+_ = t.ugettext
+
+PROC_NAME = 'python-fu-console'
+
+RESPONSE_BROWSE, RESPONSE_CLEAR, RESPONSE_SAVE = range(3)
+
+def do_console():
+ import pygtk
+ pygtk.require('2.0')
+
+ import sys, gobject, gtk, gimpenums, gimpshelf, gimpui, pyconsole
+ gimpui.gimp_ui_init ()
+
+ namespace = {'__builtins__': __builtins__,
+ '__name__': '__main__', '__doc__': None,
+ 'gimp': gimp, 'pdb': gimp.pdb,
+ 'shelf': gimpshelf.shelf}
+
+ for s in gimpenums.__dict__.keys():
+ if s[0] != '_':
+ namespace[s] = getattr(gimpenums, s)
+
+ class GimpConsole(pyconsole.Console):
+ def __init__(self, quit_func=None):
+ banner = ('GIMP %s Python Console\nPython %s\n' %
+ (gimp.pdb.gimp_version(), sys.version))
+ pyconsole.Console.__init__(self,
+ locals=namespace, banner=banner,
+ quit_func=quit_func)
+ def _commit(self):
+ pyconsole.Console._commit(self)
+ gimp.displays_flush()
+
+ class ConsoleDialog(gimpui.Dialog):
+ def __init__(self):
+ gimpui.Dialog.__init__(self, title=_("Python Console"),
+ role=PROC_NAME, help_id=PROC_NAME,
+ buttons=(gtk.STOCK_SAVE, RESPONSE_SAVE,
+ gtk.STOCK_CLEAR, RESPONSE_CLEAR,
+ _("_Browse..."), RESPONSE_BROWSE,
+ gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE))
+
+ self.set_name (PROC_NAME)
+ self.set_alternative_button_order((gtk.RESPONSE_CLOSE,
+ RESPONSE_BROWSE,
+ RESPONSE_CLEAR,
+ RESPONSE_SAVE))
+
+ self.cons = GimpConsole(quit_func=lambda: gtk.main_quit())
+
+ self.style_set (None, None)
+
+ self.connect('response', self.response)
+ self.connect('style-set', self.style_set)
+
+ self.browse_dlg = None
+ self.save_dlg = None
+
+ vbox = gtk.VBox(False, 12)
+ vbox.set_border_width(12)
+ self.vbox.pack_start(vbox)
+
+ scrl_win = gtk.ScrolledWindow()
+ scrl_win.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
+ vbox.pack_start(scrl_win)
+
+ scrl_win.add(self.cons)
+
+ width, height = self.cons.get_default_size()
+ sb_width, sb_height = scrl_win.get_vscrollbar().size_request()
+
+ # Account for scrollbar width and border width to ensure
+ # the text view gets a width of 80 characters. We don't care
+ # so much whether the height will be exactly 40 characters.
+ self.set_default_size(width + sb_width + 2 * 12, height)
+
+ def style_set(self, old_style, user_data):
+ style = self.get_style ()
+ self.cons.stdout_tag.set_property ("foreground", style.text[gtk.STATE_NORMAL])
+ self.cons.stderr_tag.set_property ("foreground", style.text[gtk.STATE_INSENSITIVE])
+
+ def response(self, dialog, response_id):
+ if response_id == RESPONSE_BROWSE:
+ self.browse()
+ elif response_id == RESPONSE_CLEAR:
+ self.cons.banner = None
+ self.cons.clear()
+ elif response_id == RESPONSE_SAVE:
+ self.save_dialog()
+ else:
+ gtk.main_quit()
+
+ self.cons.grab_focus()
+
+ def browse_response(self, dlg, response_id):
+ if response_id != gtk.RESPONSE_APPLY:
+ dlg.hide()
+ return
+
+ proc_name = dlg.get_selected()
+
+ if not proc_name:
+ return
+
+ proc = pdb[proc_name]
+
+ cmd = ''
+
+ if len(proc.return_vals) > 0:
+ cmd = ', '.join([x[1].replace('-', '_')
+ for x in proc.return_vals]) + ' = '
+
+ cmd = cmd + 'pdb.%s' % proc.proc_name.replace('-', '_')
+
+ if len(proc.params) > 0 and proc.params[0][1] == 'run-mode':
+ params = proc.params[1:]
+ else:
+ params = proc.params
+
+ cmd = cmd + '(%s)' % ', '.join([x[1].replace('-', '_')
+ for x in params])
+
+ buffer = self.cons.buffer
+
+ lines = buffer.get_line_count()
+ iter = buffer.get_iter_at_line_offset(lines - 1, 4)
+ buffer.delete(iter, buffer.get_end_iter())
+ buffer.place_cursor(buffer.get_end_iter())
+ buffer.insert_at_cursor(cmd)
+
+ def browse(self):
+ if not self.browse_dlg:
+ dlg = gimpui.ProcBrowserDialog(_("Python Procedure Browser"),
+ role=PROC_NAME,
+ buttons=(gtk.STOCK_APPLY,
+ gtk.RESPONSE_APPLY,
+ gtk.STOCK_CLOSE,
+ gtk.RESPONSE_CLOSE))
+
+ dlg.set_default_response(gtk.RESPONSE_APPLY)
+ dlg.set_alternative_button_order((gtk.RESPONSE_CLOSE,
+ gtk.RESPONSE_APPLY))
+
+ dlg.connect('response', self.browse_response)
+ dlg.connect('row-activated',
+ lambda dlg: dlg.response(gtk.RESPONSE_APPLY))
+
+ self.browse_dlg = dlg
+
+ self.browse_dlg.present()
+
+ def save_response(self, dlg, response_id):
+ if response_id == gtk.RESPONSE_DELETE_EVENT:
+ self.save_dlg = None
+ return
+ elif response_id == gtk.RESPONSE_OK:
+ filename = dlg.get_filename()
+
+ try:
+ logfile = open(filename, 'w')
+ except IOError, e:
+ gimp.message(_("Could not open '%s' for writing: %s") %
+ (filename, e.strerror))
+ return
+
+ buffer = self.cons.buffer
+
+ start = buffer.get_start_iter()
+ end = buffer.get_end_iter()
+
+ log = buffer.get_text(start, end, False)
+
+ try:
+ logfile.write(log)
+ logfile.close()
+ except IOError, e:
+ gimp.message(_("Could not write to '%s': %s") %
+ (filename, e.strerror))
+ return
+
+ dlg.hide()
+
+ def save_dialog(self):
+ if not self.save_dlg:
+ dlg = gtk.FileChooserDialog(_("Save Python-Fu Console Output"),
+ parent=self,
+ action=gtk.FILE_CHOOSER_ACTION_SAVE,
+ buttons=(gtk.STOCK_CANCEL,
+ gtk.RESPONSE_CANCEL,
+ gtk.STOCK_SAVE,
+ gtk.RESPONSE_OK))
+
+ dlg.set_default_response(gtk.RESPONSE_OK)
+ dlg.set_alternative_button_order((gtk.RESPONSE_OK,
+ gtk.RESPONSE_CANCEL))
+
+ dlg.connect('response', self.save_response)
+
+ self.save_dlg = dlg
+
+ self.save_dlg.present()
+
+ def run(self):
+ self.show_all()
+ gtk.main()
+
+ ConsoleDialog().run()
+
+register(
+ PROC_NAME,
+ N_("Interactive GIMP Python interpreter"),
+ "Type in commands and see results",
+ "James Henstridge",
+ "James Henstridge",
+ "1997-1999",
+ N_("_Console"),
+ "",
+ [],
+ [],
+ do_console,
+ menu="<Image>/Filters/Languages/Python-Fu",
+ domain=("gimp20-python", gimp.locale_directory))
+
+main()
diff --git a/plug-ins/pygimp/plug-ins/python-eval.py b/plug-ins/pygimp/plug-ins/python-eval.py
new file mode 100755
index 0000000..9c1663d
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/python-eval.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python2
+
+# Gimp-Python - allows the writing of Gimp plugins in Python.
+# Copyright (C) 2006 Manish Singh <yosh@gimp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+from gimpfu import *
+
+def code_eval(code):
+ if code == '-':
+ import sys
+ code = sys.stdin
+ exec code in globals()
+
+register(
+ "python-fu-eval",
+ "Evaluate Python code",
+ "Evaluate python code under the python interpreter (primarily for batch mode)",
+ "Manish Singh",
+ "Manish Singh",
+ "2006",
+ None,
+ None,
+ [
+ (PF_STRING, "code", "The code to evaluate", "")
+ ],
+ [],
+ code_eval)
+
+main()
diff --git a/plug-ins/pygimp/plug-ins/shadow_bevel.py b/plug-ins/pygimp/plug-ins/shadow_bevel.py
new file mode 100755
index 0000000..29a9246
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/shadow_bevel.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python2
+
+# Gimp-Python - allows the writing of Gimp plugins in Python.
+# Copyright (C) 1997 James Henstridge <james@daa.com.au>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+from gimpfu import *
+
+gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
+
+def shadow_bevel(img, drawable, blur, bevel, do_shadow, drop_x, drop_y):
+ # disable undo for the image
+ img.undo_group_start()
+
+ # copy the layer
+ shadow = drawable.copy(True)
+ img.insert_layer(shadow, position=img.layers.index(drawable) + 1)
+ shadow.name = drawable.name + " shadow"
+ shadow.lock_alpha = False
+
+ # threshold the shadow layer to all white
+ pdb.gimp_threshold(shadow, 0, 255)
+
+ # blur the shadow layer
+ pdb.plug_in_gauss_iir(img, shadow, blur, True, True)
+
+ # do the bevel thing ...
+ if bevel:
+ pdb.plug_in_bump_map(img, drawable, shadow, 135, 45, 3,
+ 0, 0, 0, 0, True, False, 0)
+
+ # make the shadow layer black now ...
+ pdb.gimp_drawable_invert(shadow, False)
+
+ # translate the drop shadow
+ shadow.translate(drop_x, drop_y)
+
+ if not do_shadow:
+ # delete shadow ...
+ gimp.delete(shadow)
+
+ # enable undo again
+ img.undo_group_end()
+
+
+register(
+ "python-fu-shadow-bevel",
+ N_("Add a drop shadow to a layer, and optionally bevel it"),
+ "Add a drop shadow to a layer, and optionally bevel it",
+ "James Henstridge",
+ "James Henstridge",
+ "1999",
+ N_("_Drop Shadow and Bevel..."),
+ "RGBA, GRAYA",
+ [
+ (PF_IMAGE, "image", "Input image", None),
+ (PF_DRAWABLE, "drawable", "Input drawable", None),
+ (PF_SLIDER, "blur", _("_Shadow blur"), 6, (1, 30, 1)),
+ (PF_BOOL, "bevel", _("_Bevel"), True),
+ (PF_BOOL, "shadow", _("_Drop shadow"), True),
+ (PF_INT, "drop-x", _("Drop shadow _X displacement"), 3),
+ (PF_INT, "drop-y", _("Drop shadow _Y displacement"), 6)
+ ],
+ [],
+ shadow_bevel,
+ menu="<Image>/Filters/Light and Shadow/Shadow",
+ domain=("gimp20-python", gimp.locale_directory)
+ )
+
+main()
diff --git a/plug-ins/pygimp/plug-ins/sphere.py b/plug-ins/pygimp/plug-ins/sphere.py
new file mode 100755
index 0000000..889c3f7
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/sphere.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python2
+
+# Gimp-Python - allows the writing of Gimp plugins in Python.
+# Copyright (C) 1997 James Henstridge <james@daa.com.au>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import math
+from gimpfu import *
+
+def sphere(radius, light, shadow, foo, bg_colour, sphere_colour):
+ if radius < 1:
+ radius = 1
+
+ width = int(radius * 3.75)
+ height = int(radius * 2.5)
+
+ gimp.context_push()
+
+ img = gimp.Image(width, height, RGB)
+
+ drawable = gimp.Layer(img, "Sphere Layer", width, height,
+ RGB_IMAGE, 100, NORMAL_MODE)
+
+ radians = light * math.pi / 180
+
+ cx = width / 2
+ cy = height / 2
+
+ light_x = cx + radius * 0.6 * math.cos(radians)
+ light_y = cy - radius * 0.6 * math.sin(radians)
+
+ light_end_x = cx + radius * math.cos(math.pi + radians)
+ light_end_y = cy - radius * math.sin(math.pi + radians)
+
+ offset = radius * 0.1
+
+ img.disable_undo()
+ img.insert_layer(drawable)
+
+ gimp.set_foreground(sphere_colour)
+
+ gimp.set_background(bg_colour)
+ pdb.gimp_edit_fill(drawable, BACKGROUND_FILL)
+
+ gimp.set_background(20, 20, 20)
+
+ if (light >= 45 and light <= 75 or light <= 135 and
+ light >= 105) and shadow:
+ shadow_w = radius * 2.5 * math.cos(math.pi + radians)
+ shadow_h = radius * 0.5
+ shadow_x = cx
+ shadow_y = cy + radius * 0.65
+
+ if shadow_w < 0:
+ shadow_x = cx + shadow_w
+ shadow_w = -shadow_w
+
+ pdb.gimp_ellipse_select(img, shadow_x, shadow_y, shadow_w, shadow_h,
+ CHANNEL_OP_REPLACE, True, True, 7.5)
+ pdb.gimp_edit_bucket_fill(drawable, BG_BUCKET_FILL,
+ MULTIPLY_MODE, 100, 0, False, 0, 0)
+
+ pdb.gimp_ellipse_select(img, cx - radius, cy - radius, 2 * radius,
+ 2 * radius, CHANNEL_OP_REPLACE, True, False, 0)
+ pdb.gimp_edit_blend(drawable, FG_BG_RGB_MODE, NORMAL_MODE, GRADIENT_RADIAL,
+ 100, offset, REPEAT_NONE, False, False, 0, 0, True,
+ light_x, light_y, light_end_x, light_end_y)
+
+ pdb.gimp_selection_none(img)
+
+ img.enable_undo()
+
+ disp = gimp.Display(img)
+
+ gimp.context_pop()
+
+
+register(
+ "python-fu-sphere",
+ "Simple sphere with drop shadow",
+ "Simple sphere with drop shadow",
+ "James Henstridge",
+ "James Henstridge",
+ "1997-1999, 2007",
+ "_Sphere",
+ "",
+ [
+ (PF_INT, "radius", "Radius for sphere", 100),
+ (PF_SLIDER, "light", "Light angle", 45, (0,360,1)),
+ (PF_TOGGLE, "shadow", "Shadow?", 1),
+ (PF_RADIO, "foo", "Test", "foo", (("Foo", "foo"), ("Bar", "bar"))),
+ (PF_COLOR, "bg-color", "Background", (1.0, 1.0, 1.0)),
+ (PF_COLOR, "sphere-color", "Sphere", "orange")
+ ],
+ [],
+ sphere,
+ menu="<Image>/Filters/Languages/Python-Fu/Test")
+
+main()
diff --git a/plug-ins/pygimp/plug-ins/spyro_plus.py b/plug-ins/pygimp/plug-ins/spyro_plus.py
new file mode 100644
index 0000000..7d31c54
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/spyro_plus.py
@@ -0,0 +1,2212 @@
+#!/usr/bin/env python2
+
+# Draw Spyrographs, Epitrochoids, and Lissajous curves with interactive feedback.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+from gimpshelf import shelf
+from gimpenums import *
+import gimp
+import gimpplugin
+import gimpui
+import gobject
+import gtk
+gdk = gtk.gdk
+
+from math import pi, sin, cos, atan, atan2, fmod, radians, sqrt
+import gettext
+import fractions
+import time
+
+
+# i18n
+t = gettext.translation("gimp20-python", gimp.locale_directory, fallback=True)
+_ = t.ugettext
+
+def N_(message):
+ return message
+
+
+pdb = gimp.pdb
+
+two_pi, half_pi = 2 * pi, pi / 2
+layer_name = _("Spyro Layer")
+path_name = _("Spyro Path")
+
+# "Enums"
+GEAR_NOTATION, TOY_KIT_NOTATION, VISUAL_NOTATION = range(3) # Pattern notations
+
+# Mapping of pattern notation to the corresponding tab in the pattern notation notebook.
+pattern_notation_page = {}
+
+# Save options of the dialog
+SAVE_AS_NEW_LAYER, SAVE_BY_REDRAW, SAVE_AS_PATH = range(3)
+save_options = [
+ _("Save\nas New Layer"),
+ _("Redraw on\nActive layer"),
+ _("Save\nas Path")
+]
+
+ring_teeth = [96, 144, 105, 150]
+
+# Moving gear. Each gear is a pair of (#teeth, #holes)
+# Hole #1 is closest to the edge of the wheel.
+# The last hole is closest to the center.
+wheel = [
+ (24, 5), (30, 8), (32, 9), (36, 11), (40, 13), (42, 14), (45, 16),
+ (48, 17), (50, 18), (52, 19), (56, 21), (60, 23), (63, 25), (64, 25),
+ (72, 29), (75, 31), (80, 33), (84, 35)
+]
+wheel_teeth = [wh[0] for wh in wheel]
+
+
+def lcm(a, b):
+ """ Least common multiplier """
+ return a * b // fractions.gcd(a, b)
+
+
+### Shapes
+
+
+class CanRotateShape:
+ pass
+
+
+class Shape:
+ def configure(self, img, pp, cp):
+ self.image, self.pp, self.cp = img, pp, cp
+
+ def can_equal_w_h(self):
+ return True
+
+ def has_sides(self):
+ return isinstance(self, SidedShape)
+
+ def can_rotate(self):
+ return isinstance(self, CanRotateShape)
+
+ def can_morph(self):
+ return self.has_sides()
+
+
+class CircleShape(Shape):
+ name = _("Circle")
+
+ def get_center_of_moving_gear(self, oangle, dist=None):
+ """
+ :return: x,y - position where the center of the moving gear should be,
+ after going over oangle/two_pi of a full cycle over the outer gear.
+ """
+ cp = self.cp
+ if dist is None:
+ dist = cp.moving_gear_radius
+
+ return (cp.x_center + (cp.x_half_size - dist) * cos(oangle),
+ cp.y_center + (cp.y_half_size - dist) * sin(oangle))
+
+
+class SidedShape(CanRotateShape, Shape):
+
+ def configure(self, img, pp, cp):
+ Shape.configure(self, img, pp, cp)
+ self.angle_of_each_side = two_pi / pp.sides
+ self.half_angle = self.angle_of_each_side / 2.0
+ self.cos_half_angle = cos(self.half_angle)
+
+ def get_center_of_moving_gear(self, oangle, dist=None):
+ if dist is None:
+ dist = self.cp.moving_gear_radius
+ shape_factor = self.get_shape_factor(oangle)
+ return (
+ self.cp.x_center +
+ (self.cp.x_half_size - dist) * shape_factor * cos(oangle),
+ self.cp.y_center +
+ (self.cp.y_half_size - dist) * shape_factor * sin(oangle)
+ )
+
+
+class PolygonShape(SidedShape):
+ name = _("Polygon-Star")
+
+ def get_shape_factor(self, oangle):
+ oangle_mod = fmod(oangle + self.cp.shape_rotation_radians, self.angle_of_each_side)
+ if oangle_mod > self.half_angle:
+ oangle_mod = self.angle_of_each_side - oangle_mod
+
+ # When oangle_mod = 0, the shape_factor will be cos(half_angle)) - which is the minimal shape_factor.
+ # When oangle_mod is near the half_angle, the shape_factor will near 1.
+ shape_factor = self.cos_half_angle / cos(oangle_mod)
+ shape_factor -= self.pp.morph * (1 - shape_factor) * (1 + (self.pp.sides - 3) * 2)
+ return shape_factor
+
+
+class SineShape(SidedShape):
+ # Sine wave on a circle ring.
+ name = _("Sine")
+
+ def get_shape_factor(self, oangle):
+ oangle_mod = fmod(oangle + self.cp.shape_rotation_radians, self.angle_of_each_side)
+ oangle_stretched = oangle_mod * self.pp.sides
+ return 1 - self.pp.morph * (cos(oangle_stretched) + 1)
+
+
+class BumpShape(SidedShape):
+ # Semi-circles, based on a polygon
+ name = _("Bumps")
+
+ def get_shape_factor(self, oangle):
+ oangle_mod = fmod(oangle + self.cp.shape_rotation_radians, self.angle_of_each_side)
+ # Stretch back to angle between 0 and pi
+ oangle_stretched = oangle_mod/2.0 * self.pp.sides
+
+ # Compute factor for polygon.
+ poly_angle = oangle_mod
+ if poly_angle > self.half_angle:
+ poly_angle = self.angle_of_each_side - poly_angle
+ # When poly_oangle = 0, the shape_factor will be cos(half_angle)) - the minimal shape_factor.
+ # When poly_angle is near the half_angle, the shape_factor will near 1.
+ polygon_factor = self.cos_half_angle / cos(poly_angle)
+
+ # Bump
+ return polygon_factor - self.pp.morph * (1 - abs(cos(oangle_stretched)))
+
+
+class ShapePart(object):
+ def set_bounds(self, start, end):
+ self.bound_start, self.bound_end = start, end
+ self.bound_diff = self.bound_end - self.bound_start
+
+
+class StraightPart(ShapePart):
+
+ def __init__(self, teeth, perp_direction, x1, y1, x2, y2):
+ self.teeth, self.perp_direction = max(teeth, 1), perp_direction
+ self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
+ self.x_diff = self.x2 - self.x1
+ self.y_diff = self.y2 - self.y1
+
+ angle = atan2(self.y_diff, self.x_diff) # - shape_rotation_radians
+ perp_angle = angle + perp_direction * half_pi
+ self.sin_angle = sin(perp_angle)
+ self.cos_angle = cos(perp_angle)
+
+ def perpendicular_at_oangle(self, oangle, perp_distance):
+ factor = (oangle - self.bound_start) / self.bound_diff
+ return (self.x1 + factor * self.x_diff + perp_distance * self.cos_angle,
+ self.y1 + factor * self.y_diff + perp_distance * self.sin_angle)
+
+
+class RoundPart(ShapePart):
+
+ def __init__(self, teeth, x, y, start_angle, end_angle):
+ self.teeth = max(teeth, 1)
+ self.start_angle, self.end_angle = start_angle, end_angle
+ self.x, self.y = x, y
+
+ self.diff_angle = self.end_angle - self.start_angle
+
+ def perpendicular_at_oangle(self, oangle, perp_distance):
+ angle = (
+ self.start_angle +
+ self.diff_angle * (oangle - self.bound_start) / self.bound_diff
+ )
+ return (self.x + perp_distance * cos(angle),
+ self.y + perp_distance * sin(angle))
+
+
+class ShapeParts(list):
+ """ A list of shape parts. """
+
+ def __init__(self):
+ list.__init__(self)
+ self.total_teeth = 0
+
+ def finish(self):
+ for part in self:
+ self.total_teeth += part.teeth
+ teeth = 0
+ bound_end = 0.0
+ for part in self:
+ bound_start = bound_end
+ teeth += part.teeth
+ bound_end = teeth/float(self.total_teeth) * two_pi
+ part.set_bounds(bound_start, bound_end)
+
+ def perpendicular_at_oangle(self, oangle, perp_distance):
+ for part in self:
+ if oangle <= part.bound_end:
+ return part.perpendicular_at_oangle(oangle, perp_distance)
+
+ # We shouldn't reach here
+ return 0.0, 0.0
+
+
+class AbstractShapeFromParts(Shape):
+ def __init__(self):
+ self.parts = None
+
+ def get_center_of_moving_gear(self, oangle, dist=None):
+ """
+ :param oangle: an angle in radians, between 0 and 2*pi
+ :return: x,y - position where the center of the moving gear should be,
+ after going over oangle/two_pi of a full cycle over the outer gear.
+ """
+ if dist is None:
+ dist = self.cp.moving_gear_radius
+ return self.parts.perpendicular_at_oangle(oangle, dist)
+
+
+class RackShape(CanRotateShape, AbstractShapeFromParts):
+ name = _("Rack")
+
+ def configure(self, img, pp, cp):
+ Shape.configure(self, img, pp, cp)
+
+ round_teeth = 12
+ side_teeth = (cp.fixed_gear_teeth - 2 * round_teeth) / 2
+
+ # Determine start and end points of rack.
+
+ cos_rot = cos(cp.shape_rotation_radians)
+ sin_rot = sin(cp.shape_rotation_radians)
+
+ x_size = cp.x2 - cp.x1 - cp.moving_gear_radius * 4
+ y_size = cp.y2 - cp.y1 - cp.moving_gear_radius * 4
+
+ size = ((x_size * cos_rot)**2 + (y_size * sin_rot)**2) ** 0.5
+
+ x1 = cp.x_center - size/2.0 * cos_rot
+ y1 = cp.y_center - size/2.0 * sin_rot
+ x2 = cp.x_center + size/2.0 * cos_rot
+ y2 = cp.y_center + size/2.0 * sin_rot
+
+ # Build shape from shape parts.
+ self.parts = ShapeParts()
+ self.parts.append(StraightPart(side_teeth, -1, x2, y2, x1, y1))
+ self.parts.append(
+ RoundPart(
+ round_teeth, x1, y1,
+ half_pi + cp.shape_rotation_radians,
+ 3 * half_pi + cp.shape_rotation_radians
+ )
+ )
+ self.parts.append(StraightPart(side_teeth, -1, x1, y1, x2, y2))
+ self.parts.append(
+ RoundPart(
+ round_teeth, x2, y2,
+ 3 * half_pi + cp.shape_rotation_radians,
+ 5 * half_pi + cp.shape_rotation_radians)
+ )
+ self.parts.finish()
+
+
+class FrameShape(AbstractShapeFromParts):
+ name = _("Frame")
+
+ def configure(self, img, pp, cp):
+ Shape.configure(self, img, pp, cp)
+
+ x1, x2 = cp.x1 + cp.moving_gear_radius, cp.x2 - cp.moving_gear_radius
+ y1, y2 = cp.y1 + cp.moving_gear_radius, cp.y2 - cp.moving_gear_radius
+ x_diff, y_diff = abs(x2 - x1), abs(y2 - y1)
+
+ # Build shape from shape parts.
+ self.parts = ShapeParts()
+ self.parts.append(StraightPart(x_diff, 1, x2, cp.y2, x1, cp.y2))
+ self.parts.append(StraightPart(y_diff, 1, cp.x1, y2, cp.x1, y1))
+ self.parts.append(StraightPart(x_diff, 1, x1, cp.y1, x2, cp.y1))
+ self.parts.append(StraightPart(y_diff, 1, cp.x2, y1, cp.x2, y2))
+ self.parts.finish()
+
+
+class SelectionToPath:
+ """ Converts a selection to a path """
+
+ def __init__(self, image):
+ self.image = image
+
+ # Compute hash of selection, so we can detect when it was modified.
+ self.last_selection_hash = self.compute_selection_hash()
+
+ self.convert_selection_to_path()
+
+ def convert_selection_to_path(self):
+
+ if pdb.gimp_selection_is_empty(self.image):
+ selection_was_empty = True
+ pdb.gimp_selection_all(self.image)
+ else:
+ selection_was_empty = False
+
+ pdb.plug_in_sel2path(self.image, self.image.active_layer)
+
+ self.path = self.image.vectors[0]
+
+ self.num_strokes, self.stroke_ids = pdb.gimp_vectors_get_strokes(self.path)
+ self.stroke_ids = list(self.stroke_ids)
+
+ # A path may contain several strokes. If so lets throw away a stroke that
+ # simply describes the borders of the image, if one exists.
+ if self.num_strokes > 1:
+ # Lets compute what a stroke of the image borders should look like.
+ w, h = float(self.image.width), float(self.image.height)
+ frame_strokes = [0.0] * 6 + [0.0, h] * 3 + [w, h] * 3 + [w, 0.0] * 3
+
+ for stroke in range(self.num_strokes):
+ strokes = self.path.strokes[stroke].points[0]
+ if strokes == frame_strokes:
+ del self.stroke_ids[stroke]
+ self.num_strokes -= 1
+ break
+
+ self.set_current_stroke(0)
+
+ if selection_was_empty:
+ # Restore empty selection if it was empty.
+ pdb.gimp_selection_none(self.image)
+
+ def compute_selection_hash(self):
+ px = self.image.selection.get_pixel_rgn(0, 0, self.image.width, self.image.height)
+ return px[0:self.image.width, 0:self.image.height].__hash__()
+
+ def regenerate_path_if_selection_changed(self):
+ current_selection_hash = self.compute_selection_hash()
+ if self.last_selection_hash != current_selection_hash:
+ self.last_selection_hash = current_selection_hash
+ self.convert_selection_to_path()
+
+ def get_num_strokes(self):
+ return self.num_strokes
+
+ def set_current_stroke(self, stroke_id=0):
+ # Compute path length.
+ self.path_length = pdb.gimp_vectors_stroke_get_length(self.path, self.stroke_ids[stroke_id], 1.0)
+ self.current_stroke = stroke_id
+
+ def point_at_angle(self, oangle):
+ oangle_mod = fmod(oangle, two_pi)
+ dist = self.path_length * oangle_mod / two_pi
+ return pdb.gimp_vectors_stroke_get_point_at_dist(self.path, self.stroke_ids[self.current_stroke], dist, 1.0)
+
+
+class SelectionShape(Shape):
+ name = _("Selection")
+
+ def __init__(self):
+ self.path = None
+
+ def process_selection(self, img):
+ if self.path is None:
+ self.path = SelectionToPath(img)
+ else:
+ self.path.regenerate_path_if_selection_changed()
+
+ def configure(self, img, pp, cp):
+ """ Set bounds of pattern """
+ Shape.configure(self, img, pp, cp)
+ self.drawing_no = cp.current_drawing
+ self.path.set_current_stroke(self.drawing_no)
+
+ def get_num_drawings(self):
+ return self.path.get_num_strokes()
+
+ def can_equal_w_h(self):
+ return False
+
+ def get_center_of_moving_gear(self, oangle, dist=None):
+ """
+ :param oangle: an angle in radians, between 0 and 2*pi
+ :return: x,y - position where the center of the moving gear should be,
+ after going over oangle/two_pi of a full cycle over the outer gear.
+ """
+ cp = self.cp
+ if dist is None:
+ dist = cp.moving_gear_radius
+ x, y, slope, valid = self.path.point_at_angle(oangle)
+ slope_angle = atan(slope)
+ # We want to find an angle perpendicular to the slope, but in which direction?
+ # Lets try both sides and see which of them is inside the selection.
+ perpendicular_p, perpendicular_m = slope_angle + half_pi, slope_angle - half_pi
+ step_size = 2 # The distance we are going to go in the direction of each angle.
+ xp, yp = x + step_size * cos(perpendicular_p), y + step_size * sin(perpendicular_p)
+ value_plus = pdb.gimp_selection_value(self.image, xp, yp)
+ xp, yp = x + step_size * cos(perpendicular_m), y + step_size * sin(perpendicular_m)
+ value_minus = pdb.gimp_selection_value(self.image, xp, yp)
+
+ perpendicular = perpendicular_p if value_plus > value_minus else perpendicular_m
+ return x + dist * cos(perpendicular), y + dist * sin(perpendicular)
+
+
+shapes = [
+ CircleShape(), RackShape(), FrameShape(), SelectionShape(),
+ PolygonShape(), SineShape(), BumpShape()
+]
+
+
+### Tools
+
+
+def get_gradient_samples(num_samples):
+ gradient_name = pdb.gimp_context_get_gradient()
+ reverse_mode = pdb.gimp_context_get_gradient_reverse()
+ repeat_mode = pdb.gimp_context_get_gradient_repeat_mode()
+
+ if repeat_mode == REPEAT_TRIANGULAR:
+ # Get two uniform samples, which are reversed from each other, and connect them.
+
+ samples = num_samples/2 + 1
+ num, color_samples = pdb.gimp_gradient_get_uniform_samples(gradient_name,
+ samples, reverse_mode)
+
+ color_samples = list(color_samples)
+ del color_samples[-4:] # Delete last color because it will appear in the next sample
+
+ # If num_samples is odd, lets get an extra sample this time.
+ if num_samples % 2 == 1:
+ samples += 1
+
+ num, color_samples2 = pdb.gimp_gradient_get_uniform_samples(gradient_name,
+ samples, 1 - reverse_mode)
+
+ color_samples2 = list(color_samples2)
+ del color_samples2[-4:] # Delete last color because it will appear in the very first sample
+
+ color_samples.extend(color_samples2)
+ color_samples = tuple(color_samples)
+ else:
+ num, color_samples = pdb.gimp_gradient_get_uniform_samples(gradient_name, num_samples, reverse_mode)
+
+ return color_samples
+
+
+class PencilTool():
+ name = _("Pencil")
+ can_color = True
+
+ def draw(self, layer, strokes, color=None):
+ if color:
+ pdb.gimp_context_push()
+ pdb.gimp_context_set_dynamics('Dynamics Off')
+ pdb.gimp_context_set_foreground(color)
+
+ pdb.gimp_pencil(layer, len(strokes), strokes)
+
+ if color:
+ pdb.gimp_context_pop()
+
+
+class AirBrushTool():
+ name = _("AirBrush")
+ can_color = True
+
+ def draw(self, layer, strokes, color=None):
+ if color:
+ pdb.gimp_context_push()
+ pdb.gimp_context_set_dynamics('Dynamics Off')
+ pdb.gimp_context_set_foreground(color)
+
+ pdb.gimp_airbrush_default(layer, len(strokes), strokes)
+
+ if color:
+ pdb.gimp_context_pop()
+
+
+class AbstractStrokeTool():
+
+ def draw(self, layer, strokes, color=None):
+ # We need to multiply every point by 3, because we are creating a path,
+ # where each point has two additional control points.
+ control_points = []
+ for i, k in zip(strokes[0::2], strokes[1::2]):
+ control_points += [i, k] * 3
+
+ # Create path
+ path = pdb.gimp_vectors_new(layer.image, 'temp_path')
+ pdb.gimp_image_add_vectors(layer.image, path, 0)
+ sid = pdb.gimp_vectors_stroke_new_from_points(path, 0, len(control_points),
+ control_points, False)
+
+ # Draw it.
+
+ pdb.gimp_context_push()
+
+ # Call template method to set the kind of stroke to draw.
+ self.prepare_stroke_context(color)
+
+ pdb.gimp_drawable_edit_stroke_item(layer, path)
+ pdb.gimp_context_pop()
+
+ # Get rid of the path.
+ pdb.gimp_image_remove_vectors(layer.image, path)
+
+
+# Drawing tool that should be quick, for purposes of previewing the pattern.
+class PreviewTool:
+
+ # Implementation using pencil. (A previous implementation using stroke was slower, and thus removed).
+ def draw(self, layer, strokes, color=None):
+ foreground = pdb.gimp_context_get_foreground()
+ pdb.gimp_context_push()
+ pdb.gimp_context_set_defaults()
+ pdb.gimp_context_set_foreground(foreground)
+ pdb.gimp_context_set_dynamics('Dynamics Off')
+ pdb.gimp_context_set_brush('1. Pixel')
+ pdb.gimp_context_set_brush_size(1.0)
+ pdb.gimp_context_set_brush_spacing(3.0)
+ pdb.gimp_pencil(layer, len(strokes), strokes)
+ pdb.gimp_context_pop()
+
+ name = _("Preview")
+ can_color = False
+
+
+class StrokeTool(AbstractStrokeTool):
+ name = _("Stroke")
+ can_color = True
+
+ def prepare_stroke_context(self, color):
+ if color:
+ pdb.gimp_context_set_dynamics('Dynamics Off')
+ pdb.gimp_context_set_foreground(color)
+
+ pdb.gimp_context_set_stroke_method(STROKE_LINE)
+
+
+class StrokePaintTool(AbstractStrokeTool):
+ def __init__(self, name, paint_method, can_color=True):
+ self.name = name
+ self.paint_method = paint_method
+ self.can_color = can_color
+
+ def prepare_stroke_context(self, color):
+ if self.can_color and color is not None:
+ pdb.gimp_context_set_dynamics('Dynamics Off')
+ pdb.gimp_context_set_foreground(color)
+
+ pdb.gimp_context_set_stroke_method(STROKE_PAINT_METHOD)
+ pdb.gimp_context_set_paint_method(self.paint_method)
+
+
+class SaveToPathTool():
+ """ This tool cannot be chosen by the user from the tools menu.
+ We dont add this to the list of tools. """
+
+ def __init__(self, img):
+ self.path = pdb.gimp_vectors_new(img, path_name)
+ pdb.gimp_image_add_vectors(img, self.path, 0)
+
+ def draw(self, layer, strokes, color=None):
+ # We need to multiply every point by 3, because we are creating a path,
+ # where each point has two additional control points.
+ control_points = []
+ for i, k in zip(strokes[0::2], strokes[1::2]):
+ control_points += [i, k] * 3
+
+ sid = pdb.gimp_vectors_stroke_new_from_points(self.path, 0, len(control_points),
+ control_points, False)
+
+
+tools = [
+ PreviewTool(),
+ StrokePaintTool(_("PaintBrush"), "gimp-paintbrush"),
+ PencilTool(), AirBrushTool(), StrokeTool(),
+ StrokePaintTool(_("Ink"), 'gimp-ink'),
+ StrokePaintTool(_("MyPaintBrush"), 'gimp-mybrush')
+ # Clone does not work properly when an image is not set. When that happens, drawing fails, and
+ # I am unable to catch the error. This causes the plugin to crash, and subsequent problems with undo.
+ # StrokePaintTool("Clone", 'gimp-clone', False)
+]
+
+
+class PatternParameters:
+ """
+ All the parameters that define a pattern live in objects of this class.
+ If you serialize and saved this class, you should reproduce
+ the pattern that the plugin would draw.
+ """
+ def __init__(self):
+ if not hasattr(self, 'curve_type'):
+ self.curve_type = 0
+
+ # Pattern
+ if not hasattr(self, 'pattern_notation'):
+ self.pattern_notation = 0
+ if not hasattr(self, 'outer_teeth'):
+ self.outer_teeth = 96
+ if not hasattr(self, 'inner_teeth'):
+ self.inner_teeth = 36
+ if not hasattr(self, 'pattern_rotation'):
+ self.pattern_rotation = 0
+ # Location of hole as a percent of the radius of the inner gear - runs between 0 and 100.
+ # A value of 0 means, the hole is at the center of the wheel, which would produce a boring circle.
+ # A value of 100 means the edge of the wheel.
+ if not hasattr(self, 'hole_percent'):
+ self.hole_percent = 100.0
+
+ # Toy Kit parameters
+ # Hole number in Toy Kit notation. Hole #1 is at the edge of the wheel, and the last hole is
+ # near the center of the wheel, but not exactly at the center.
+ if not hasattr(self, 'hole_number'):
+ self.hole_number = 1
+ if not hasattr(self, 'kit_fixed_gear_index'):
+ self.kit_fixed_gear_index = 1
+ if not hasattr(self, 'kit_moving_gear_index'):
+ self.kit_moving_gear_index = 1
+
+ # Visual notation parameters
+ if not hasattr(self, 'petals'):
+ self.petals = 5
+ if not hasattr(self, 'petal_skip'):
+ self.petal_skip = 2
+ if not hasattr(self, 'doughnut_hole'):
+ self.doughnut_hole = 50.0
+ if not hasattr(self, 'doughnut_width'):
+ self.doughnut_width = 50.0
+
+ # Shape
+ if not hasattr(self, 'shape_index'):
+ self.shape_index = 0 # Index in the shapes array
+ if not hasattr(self, 'sides'):
+ self.sides = 5
+ if not hasattr(self, 'morph'):
+ self.morph = 0.5
+ if not hasattr(self, 'shape_rotation'):
+ self.shape_rotation = 0
+
+ if not hasattr(self, 'equal_w_h'):
+ self.equal_w_h = False
+ if not hasattr(self, 'margin_pixels'):
+ self.margin_pixels = 0 # Distance between the drawn shape, and the selection borders.
+
+ # Drawing style
+ if not hasattr(self, 'tool_index'):
+ self.tool_index = 0 # Index in the tools array.
+ if not hasattr(self, 'long_gradient'):
+ self.long_gradient = False
+
+ if not hasattr(self, 'save_option'):
+ self.save_option = SAVE_AS_NEW_LAYER
+
+ def kit_max_hole_number(self):
+ return wheel[self.kit_moving_gear_index][1]
+
+
+# Handle shelving of plugin parameters
+
+def unshelf_parameters():
+ if shelf.has_key("p"):
+ parameters = shelf["p"]
+ parameters.__init__() # Fill in missing values with defaults.
+ return parameters
+
+ return PatternParameters()
+
+
+def shelf_parameters(pp):
+ shelf["p"] = pp
+
+
+class ComputedParameters:
+ """
+ Stores computations performed on a PatternParameters object.
+ The results of these computations are used to perform the drawing.
+ Having all these computations in one place makes it convenient to pass
+ around as a parameter.
+
+ If the pattern parameters should result in multiple pattern to be drawn, the
+ compute parameters also stores which one is currently being drawn.
+ """
+
+ def __init__(self, pp, img):
+
+ def compute_gradients():
+ self.use_gradient = self.pp.long_gradient and tools[self.pp.tool_index].can_color
+
+ # If gradient is used, determine how the lines are two be split to different colors.
+ if self.use_gradient:
+ # We want to use enough samples to be beautiful, but not too many, that would
+ # force us to make many separate calls for drawing the pattern.
+ if self.rotations > 30:
+ self.chunk_num = self.rotations
+ self.chunk_size_lines = self.fixed_gear_teeth
+ else:
+ # Lets try to find a chunk size, such that it divides num_lines, and we get at least 30 chunks.
+ # In the worse case, we will just use "1"
+ for chunk_size in range(self.fixed_gear_teeth - 1, 0, -1):
+ if self.num_lines % chunk_size == 0:
+ if self.num_lines / chunk_size > 30:
+ break
+
+ self.chunk_num = self.num_lines / chunk_size
+ self.chunk_size_lines = chunk_size
+
+ self.gradients = get_gradient_samples(self.chunk_num)
+ else:
+ self.chunk_num, self.chunk_size_lines = None, None
+
+ def compute_sizes():
+ # Get rid of the margins.
+ self.x1 = x1 + pp.margin_pixels
+ self.y1 = y1 + pp.margin_pixels
+ self.x2 = x2 - pp.margin_pixels
+ self.y2 = y2 - pp.margin_pixels
+
+ # Compute size and position of the pattern
+ self.x_half_size, self.y_half_size = (self.x2 - self.x1) / 2, (self.y2 - self.y1) / 2
+ self.x_center, self.y_center = (self.x1 + self.x2) / 2.0, (self.y1 + self.y2) / 2.0
+
+ if pp.equal_w_h:
+ if self.x_half_size < self.y_half_size:
+ self.y_half_size = self.x_half_size
+ self.y1, self.y2 = self.y_center - self.y_half_size, self.y_center + self.y_half_size
+ elif self.x_half_size > self.y_half_size:
+ self.x_half_size = self.y_half_size
+ self.x1, self.x2 = self.x_center - self.x_half_size, self.x_center + self.x_half_size
+
+ # Find the distance between the hole and the center of the inner circle.
+ # To do this, we compute the size of the gears, by the number of teeth.
+ # The circumference of the outer ring is 2 * pi * outer_R = #fixed_gear_teeth * tooth size.
+ outer_R = min(self.x_half_size, self.y_half_size)
+ if self.pp.pattern_notation == VISUAL_NOTATION:
+ doughnut_width = self.pp.doughnut_width
+ if doughnut_width + self.pp.doughnut_hole > 100:
+ doughnut_width = 100.0 - self.pp.doughnut_hole
+
+ # Let R, r be the radius of fixed and moving gear, and let hp be the hole percent.
+ # Let dwp, dhp be the doughnut width and hole in percents of R.
+ # The two sides of the following equation calculate how to reach the center of the moving
+ # gear from the center of the fixed gear:
+ # I) R * (dhp/100 + dwp/100/2) = R - r
+ # The following equation expresses which r and hp would generate a doughnut of width dw.
+ # II) R * dw/100 = 2 * r * hp/100
+ # We solve the two above equations to calculate hp and r:
+ self.hole_percent = doughnut_width / (2.0 * (1 - (self.pp.doughnut_hole + doughnut_width/2.0)/100.0))
+ self.moving_gear_radius = outer_R * doughnut_width / (2 * self.hole_percent)
+ else:
+ size_of_tooth_in_pixels = two_pi * outer_R / self.fixed_gear_teeth
+ self.moving_gear_radius = size_of_tooth_in_pixels * self.moving_gear_teeth / two_pi
+
+ self.hole_dist_from_center = self.hole_percent / 100.0 * self.moving_gear_radius
+
+ self.pp = pp
+
+ # Check if the shape is made of multiple shapes, as in using Selection as fixed gear.
+ if (isinstance(shapes[self.pp.shape_index], SelectionShape) and
+ curve_types[self.pp.curve_type].supports_shapes()):
+ shapes[self.pp.shape_index].process_selection(img)
+ pdb.gimp_displays_flush()
+ self.num_drawings = shapes[self.pp.shape_index].get_num_drawings()
+ else:
+ self.num_drawings = 1
+ self.current_drawing = 0
+
+ # Get bounds. We don't care weather a selection exists or not.
+ exists, x1, y1, x2, y2 = pdb.gimp_selection_bounds(img)
+
+ # Combine different ways to specify patterns, into a unified set of computed parameters.
+ self.num_notation_drawings = 1
+ self.current_notation_drawing = 0
+ if self.pp.pattern_notation == GEAR_NOTATION:
+ self.fixed_gear_teeth = int(round(pp.outer_teeth))
+ self.moving_gear_teeth = int(round(pp.inner_teeth))
+ self.petals = self.num_petals()
+ self.hole_percent = pp.hole_percent
+ elif self.pp.pattern_notation == TOY_KIT_NOTATION:
+ self.fixed_gear_teeth = ring_teeth[pp.kit_fixed_gear_index]
+ self.moving_gear_teeth = wheel[pp.kit_moving_gear_index][0]
+ self.petals = self.num_petals()
+ # We want to map hole #1 to 100% and hole of max_hole_number to 2.5%
+ # We don't want 0% because that would be the exact center of the moving gear,
+ # and that would create a boring pattern.
+ max_hole_number = wheel[pp.kit_moving_gear_index][1]
+ self.hole_percent = (max_hole_number - pp.hole_number) / float(max_hole_number - 1) * 97.5 + 2.5
+ elif self.pp.pattern_notation == VISUAL_NOTATION:
+ self.petals = pp.petals
+ self.fixed_gear_teeth = pp.petals
+ self.moving_gear_teeth = pp.petals - pp.petal_skip
+ if self.moving_gear_teeth < 20:
+ self.fixed_gear_teeth *= 10
+ self.moving_gear_teeth *= 10
+ self.hole_percent = 100.0
+ self.num_notation_drawings = fractions.gcd(pp.petals, pp.petal_skip)
+ self.notation_drawings_rotation = two_pi/pp.petals
+
+ # Rotations
+ self.shape_rotation_radians = self.radians_from_degrees(pp.shape_rotation)
+ self.pattern_rotation_start_radians = self.radians_from_degrees(pp.pattern_rotation)
+ self.pattern_rotation_radians = self.pattern_rotation_start_radians
+ # Additional fixed pattern rotation for lissajous.
+ self.lissajous_rotation = two_pi/self.petals/4.0
+
+ # Compute the total number of teeth we have to go over.
+ # Another way to view it is the total of lines we are going to draw.
+ # To find this we compute the Least Common Multiplier.
+ self.num_lines = lcm(self.fixed_gear_teeth, self.moving_gear_teeth)
+ # The number of points we are going to compute. This is the number of lines, plus 1, because to draw
+ # a line we need two points.
+ self.num_points = self.num_lines + 1
+
+ # Compute gradients.
+
+ # The number or rotations needed in order to complete the pattern.
+ # Each rotation has cp.fixed_gear_teeth points + 1 points.
+ self.rotations = self.num_lines / self.fixed_gear_teeth
+
+ compute_gradients()
+
+ # Computations needed for the actual drawing of the patterns - how much should we advance each angle
+ # in each step of the computation.
+
+ # How many radians is each tooth of outer gear. This is also the amount that we
+ # will step in the iterations that generate the points of the pattern.
+ self.oangle_factor = two_pi / self.fixed_gear_teeth
+ # How many radians should the moving gear be moved, for each tooth of the fixed gear
+ angle_factor = curve_types[pp.curve_type].get_angle_factor(self)
+ self.iangle_factor = self.oangle_factor * angle_factor
+
+ compute_sizes()
+
+ def num_petals(self):
+ """ The number of 'petals' (or points) that will be produced by a spirograph drawing. """
+ return lcm(self.fixed_gear_teeth, self.moving_gear_teeth) / self.moving_gear_teeth
+
+ def radians_from_degrees(self, degrees):
+ positive_degrees = degrees if degrees >= 0 else degrees + 360
+ return radians(positive_degrees)
+
+ def get_color(self, n):
+ return self.gradients[4*n:4*(n+1)]
+
+ def next_drawing(self):
+ """ Multiple drawings can be drawn either when the selection is used as a fixed
+ gear, and/or the visual tab is used, which causes multiple drawings
+ to be drawn at different rotations. """
+ if self.current_notation_drawing < self.num_notation_drawings - 1:
+ self.current_notation_drawing += 1
+ self.pattern_rotation_radians = self.pattern_rotation_start_radians + (
+ self.current_notation_drawing * self.notation_drawings_rotation)
+ else:
+ self.current_drawing += 1
+ self.current_notation_drawing = 0
+ self.pattern_rotation_radians = self.pattern_rotation_start_radians
+
+ def has_more_drawings(self):
+ return (self.current_notation_drawing < self.num_notation_drawings - 1 or
+ self.current_drawing < self.num_drawings - 1)
+
+
+### Curve types
+
+
+class CurveType:
+
+ def supports_shapes(self):
+ return True
+
+class RouletteCurveType(CurveType):
+
+ def get_strokes(self, p, cp):
+ strokes = []
+ for curr_tooth in range(cp.num_points):
+ iangle = fmod(curr_tooth * cp.iangle_factor + cp.pattern_rotation_radians, two_pi)
+ oangle = fmod(curr_tooth * cp.oangle_factor + cp.pattern_rotation_radians, two_pi)
+
+ x, y = shapes[p.shape_index].get_center_of_moving_gear(oangle)
+ strokes.append(x + cp.hole_dist_from_center * cos(iangle))
+ strokes.append(y + cp.hole_dist_from_center * sin(iangle))
+
+ return strokes
+
+
+class SpyroCurveType(RouletteCurveType):
+ name = _("Spyrograph")
+
+ def get_angle_factor(self, cp):
+ return - (cp.fixed_gear_teeth - cp.moving_gear_teeth) / float(cp.moving_gear_teeth)
+
+
+class EpitrochoidCurvetype(RouletteCurveType):
+ name = _("Epitrochoid")
+
+ def get_angle_factor(self, cp):
+ return (cp.fixed_gear_teeth + cp.moving_gear_teeth) / float(cp.moving_gear_teeth)
+
+
+class SineCurveType(CurveType):
+ name = _("Sine")
+
+ def get_angle_factor(self, cp):
+ return cp.fixed_gear_teeth / float(cp.moving_gear_teeth)
+
+ def get_strokes(self, p, cp):
+ strokes = []
+ for curr_tooth in range(cp.num_points):
+ iangle = curr_tooth * cp.iangle_factor
+ oangle = fmod(curr_tooth * cp.oangle_factor + cp.pattern_rotation_radians, two_pi)
+
+ dist = cp.moving_gear_radius + sin(iangle) * cp.hole_dist_from_center
+ x, y = shapes[p.shape_index].get_center_of_moving_gear(oangle, dist)
+ strokes.append(x)
+ strokes.append(y)
+
+ return strokes
+
+
+class LissaCurveType:
+ name = _("Lissajous")
+
+ def get_angle_factor(self, cp):
+ return cp.fixed_gear_teeth / float(cp.moving_gear_teeth)
+
+ def get_strokes(self, p, cp):
+ strokes = []
+ for curr_tooth in range(cp.num_points):
+ iangle = curr_tooth * cp.iangle_factor
+ # Adding the cp.lissajous_rotation rotation makes the pattern have the same number of curves
+ # as the other curve types. Without it, many lissajous patterns would redraw the same lines twice,
+ # and thus look less dense than the other curves.
+ oangle = fmod(curr_tooth * cp.oangle_factor + cp.pattern_rotation_radians + cp.lissajous_rotation, two_pi)
+
+ strokes.append(cp.x_center + cp.x_half_size * cos(oangle))
+ strokes.append(cp.y_center + cp.y_half_size * cos(iangle))
+
+ return strokes
+
+ def supports_shapes(self):
+ return False
+
+
+curve_types = [SpyroCurveType(), EpitrochoidCurvetype(), SineCurveType(), LissaCurveType()]
+
+# Drawing engine. Also implements drawing incrementally.
+# We don't draw the entire stroke, because it could take several seconds,
+# Instead, we break it into chunks. Incremental drawing is also used for drawing gradients.
+class DrawingEngine:
+
+ def __init__(self, img, p):
+ self.img, self.p = img, p
+ self.cp = None
+
+ # For incremental drawing
+ self.strokes = []
+ self.start = 0
+ self.chunk_size_lines = 600
+ self.chunk_no = 0
+ # We are aiming for the drawing time of a chunk to be no longer than max_time.
+ self.max_time_sec = 0.1
+
+ self.dynamic_chunk_size = True
+
+ def pre_draw(self):
+ """ Needs to be called before starting to draw a pattern. """
+
+ self.cp = ComputedParameters(self.p, self.img)
+
+ def draw_full(self, layer):
+ """ Non incremental drawing. """
+
+ self.pre_draw()
+ self.img.undo_group_start()
+
+ while true:
+ self.set_strokes()
+
+ if self.cp.use_gradient:
+ while self.has_more_strokes():
+ self.draw_next_chunk(layer, fetch_next_drawing=False)
+ else:
+ tools[self.p.tool_index].draw(layer, self.strokes)
+
+ if self.cp.has_more_drawings():
+ self.cp.next_drawing()
+ else:
+ break
+
+ self.img.undo_group_end()
+
+ pdb.gimp_displays_flush()
+
+ # Methods for incremental drawing.
+
+ def draw_next_chunk(self, layer, fetch_next_drawing=True, tool=None):
+ stroke_chunk, color = self.next_chunk(fetch_next_drawing)
+ if not tool:
+ tool = tools[self.p.tool_index]
+ tool.draw(layer, stroke_chunk, color)
+ return len(stroke_chunk)
+
+ def set_strokes(self):
+ """ Compute the strokes of the current pattern. The heart of the plugin. """
+
+ shapes[self.p.shape_index].configure(self.img, self.p, self.cp)
+
+ self.strokes = curve_types[self.p.curve_type].get_strokes(self.p, self.cp)
+
+ self.start = 0
+ self.chunk_no = 0
+
+ if self.cp.use_gradient:
+ self.chunk_size_lines = self.cp.chunk_size_lines
+ self.dynamic_chunk_size = False
+ else:
+ self.dynamic_chunk_size = True
+
+ def reset_incremental(self):
+ """ Setup incremental drawing to start drawing from scratch. """
+ self.pre_draw()
+ self.set_strokes()
+
+ def next_chunk(self, fetch_next_drawing):
+
+ # chunk_size_lines, is the number of lines we want to draw. We need 1 extra point to draw that.
+ end = self.start + (self.chunk_size_lines + 1) * 2
+ if end > len(self.strokes):
+ end = len(self.strokes)
+ result = self.strokes[self.start:end]
+ # Promote the start to the last point. This is the start of the first line to draw next time.
+ self.start = end - 2
+ color = self.cp.get_color(self.chunk_no) if self.cp.use_gradient else None
+
+ self.chunk_no += 1
+
+ # If self.strokes has ended, lets fetch strokes for the next drawing.
+ if fetch_next_drawing and not self.has_more_strokes():
+ if self.cp.has_more_drawings():
+ self.cp.next_drawing()
+ self.set_strokes()
+
+ return result, color
+
+ def has_more_strokes(self):
+ return self.start + 2 < len(self.strokes)
+
+ # Used for displaying progress.
+ def fraction_done(self):
+ return (self.start + 2.0) / len(self.strokes)
+
+ def report_time(self, time_sec):
+ """
+ Report the time it took, in seconds, to draw the last stroke chunk.
+ This helps to determine the size of chunks to return in future calls of 'next_chunk',
+ since we want the calls to be short, to not make the user interface feel stuck.
+ """
+ if time_sec != 0 and self.dynamic_chunk_size:
+ self.chunk_size_lines = int(self.chunk_size_lines * self.max_time_sec / time_sec)
+ # Don't let chunk size be too large or small.
+ self.chunk_size_lines = max(10, self.chunk_size_lines)
+ self.chunk_size_lines = min(1000, self.chunk_size_lines)
+
+
+# Constants for DoughnutWidget
+
+# Enum - When the mouse is pressed, which target value is being changed.
+TARGET_NONE, TARGET_HOLE, TARGET_WIDTH = range(3)
+
+CIRCLE_CENTER_X = 4
+RIGHT_MARGIN = 2
+TOTAL_MARGIN = CIRCLE_CENTER_X + RIGHT_MARGIN
+
+# A widget for displaying and setting the pattern of a spirograph, using a "doughnut" as
+# a visual metaphore. This widget replaces two scale widgets.
+class DoughnutWidget(gtk.DrawingArea):
+ __gtype_name__ = 'DoughnutWidget'
+
+ def __init__(self, *args, **kwds):
+ super(DoughnutWidget, self).__init__(*args, **kwds)
+ self.set_size_request(80, 40)
+
+ self.add_events(
+ gdk.BUTTON1_MOTION_MASK |
+ gdk.BUTTON_PRESS_MASK |
+ gdk.BUTTON_RELEASE_MASK |
+ gdk.POINTER_MOTION_MASK
+ )
+
+ self.default_cursor = self.get_screen().get_root_window().get_cursor()
+ self.resize_cursor = gdk.Cursor(gdk.SB_H_DOUBLE_ARROW)
+
+ self.button_pressed = False
+ self.target = TARGET_NONE
+
+ self.hole_radius = 30
+ self.doughnut_width = 30
+ self.connect("expose-event", self.expose)
+
+ def set_hole_radius(self, hole_radius):
+ self.queue_draw()
+ self.hole_radius = hole_radius
+
+ def get_hole_radius(self):
+ return self.hole_radius
+
+ def set_width(self, width):
+ self.queue_draw()
+ self.doughnut_width = width
+
+ def get_width(self):
+ return self.doughnut_width
+
+ def compute_doughnut(self):
+ """ Compute the location of the doughnut circles.
+ Returns (circle center x, circle center y, radius of inner circle, radius of outer circle) """
+ allocation = self.get_allocation()
+ alloc_width = allocation.width - TOTAL_MARGIN
+ return (
+ CIRCLE_CENTER_X, allocation.height / 2,
+ alloc_width * self.hole_radius / 100.0,
+ alloc_width * min(self.hole_radius + self.doughnut_width, 100.0) / 100.0
+ )
+
+ def set_cursor_h_resize(self):
+ """Set the mouse to be a double arrow."""
+ gdk_window = self.get_window()
+ gdk_window.set_cursor(self.resize_cursor)
+
+ def set_default_cursor(self):
+ gdk_window = self.get_window()
+ gdk_window.set_cursor(self.default_cursor)
+
+ def get_target(self, x, y):
+ # Find out if x, y is over one of the circle edges.
+
+ center_x, center_y, hole_radius, outer_radius = self.compute_doughnut()
+
+ # Compute distance from circle center to point
+ dist = sqrt((center_x - x) ** 2 + (center_y - y) ** 2)
+
+ if abs(dist - hole_radius) <= 3:
+ return TARGET_HOLE
+ if abs(dist - outer_radius) <= 3:
+ return TARGET_WIDTH
+
+ return TARGET_NONE
+
+ def expose(self, widget, event):
+
+ cr = widget.window.cairo_create()
+ center_x, center_y, hole_radius, outer_radius = self.compute_doughnut()
+ fg_color = gtk.widget_get_default_style().fg[gtk.STATE_NORMAL]
+
+ # Draw doughnut interior
+ arc = pi * 3 / 2.0
+ cr.set_source_rgba(fg_color.red, fg_color.green, fg_color.blue, 0.5)
+ cr.arc(center_x, center_y, hole_radius, -arc, arc)
+ cr.arc_negative(center_x, center_y, outer_radius, arc, -arc)
+ cr.close_path()
+ cr.fill()
+
+ # Draw doughnut border.
+ cr.set_source_rgb(fg_color.red, fg_color.green, fg_color.blue)
+ cr.set_line_width(3)
+ cr.arc_negative(center_x, center_y, outer_radius, arc, -arc)
+ cr.stroke()
+ if hole_radius < 1.0:
+ # If the radius is too small, nothing will be drawn, so draw a small cross marker instead.
+ cr.set_line_width(2)
+ cr.move_to(center_x - 4, center_y)
+ cr.line_to(center_x + 4, center_y)
+ cr.move_to(center_x, center_y - 4)
+ cr.line_to(center_x, center_y + 4)
+ else:
+ cr.arc(center_x, center_y, hole_radius, -arc, arc)
+ cr.stroke()
+
+ def compute_new_radius(self, x):
+ """ This method is called during mouse dragging of the widget.
+ Compute the new radius based on the current x location of the mouse pointer. """
+ allocation = self.get_allocation()
+
+ # How much does a single pixel difference in x, change the radius?
+ # Note that: allocation.width - TOTAL_MARGIN = 100 radius units,
+ radius_per_pixel = 100.0 / (allocation.width - TOTAL_MARGIN)
+ new_radius = self.start_radius + (x - self.start_x) * radius_per_pixel
+
+ if self.target == TARGET_HOLE:
+ self.hole_radius = max(min(new_radius, 99.0), 0.0)
+ else:
+ self.doughnut_width = max(min(new_radius, 100.0), 1.0)
+
+ self.queue_draw()
+
+ def do_button_press_event(self, event):
+ self.button_pressed = True
+
+ # If we clicked on one of the doughnut borders, remember which
+ # border we clicked on, and setup variable to start dragging it.
+ target = self.get_target(event.x, event.y)
+ if target == TARGET_HOLE or target == TARGET_WIDTH:
+ self.target = target
+ self.start_x = event.x
+ self.start_radius = (
+ self.hole_radius if target == TARGET_HOLE else
+ self.doughnut_width
+ )
+
+ def do_button_release_event(self, event):
+ # If one the doughnut borders was being dragged, recompute the doughnut size.
+ if self.target != TARGET_NONE:
+ self.compute_new_radius(event.x)
+ # Clip the width, if it is too large to fit.
+ if self.hole_radius + self.doughnut_width > 100:
+ self.doughnut_width = 100 - self.hole_radius
+ self.emit("values_changed", self.hole_radius, self.doughnut_width)
+
+ self.button_pressed = False
+ self.target = TARGET_NONE
+
+ def do_motion_notify_event(self, event):
+ if self.button_pressed:
+ # We are dragging one of the doughnut borders; recompute its size.
+ if self.target != TARGET_NONE:
+ self.compute_new_radius(event.x)
+ else:
+ # Set cursor according to whether we are over one of the
+ # doughnut borders.
+ target = self.get_target(event.x, event.y)
+ if target == TARGET_NONE:
+ self.set_default_cursor()
+ else:
+ self.set_cursor_h_resize()
+
+
+# Create signal that returns change parameters.
+gobject.type_register(DoughnutWidget)
+gobject.signal_new("values_changed", DoughnutWidget, gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE, (gobject.TYPE_INT, gobject.TYPE_INT))
+
+
+class SpyroWindow(gtk.Window):
+
+ # Define signal to catch escape key.
+ __gsignals__ = dict(
+ myescape=(gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION,
+ None, # return type
+ (str,)) # arguments
+ )
+
+ class MyScale():
+ """ Combintation of scale and spin that control the same adjuster. """
+ def __init__(self, scale, spin):
+ self.scale, self.spin = scale, spin
+
+ def set_sensitive(self, val):
+ self.scale.set_sensitive(val)
+ self.spin.set_sensitive(val)
+
+ def __init__(self, img, layer):
+
+ def add_horizontal_separator(vbox):
+ hsep = gtk.HSeparator()
+ vbox.add(hsep)
+ hsep.show()
+
+ def add_vertical_space(vbox, height):
+ hbox = gtk.HBox()
+ hbox.set_border_width(height/2)
+ vbox.add(hbox)
+ hbox.show()
+
+ def add_to_box(box, w):
+ box.add(w)
+ w.show()
+
+ def create_table(rows, columns, border_width):
+ table = gtk.Table(rows=rows, columns=columns, homogeneous=False)
+ table.set_border_width(border_width)
+ table.set_col_spacings(10)
+ table.set_row_spacings(10)
+ return table
+
+ def label_in_table(label_text, table, row, tooltip_text=None, col=0, col_add=1):
+ """ Create a label and set it in first col of table. """
+ label = gtk.Label(label_text)
+ label.set_alignment(xalign=0.0, yalign=1.0)
+ if tooltip_text:
+ label.set_tooltip_text(tooltip_text)
+ table.attach(label, col, col + col_add, row, row + 1, xoptions=gtk.FILL, yoptions=0)
+ label.show()
+
+ def spin_in_table(adj, table, row, callback, digits=0, col=0):
+ spin = gtk.SpinButton(adj, climb_rate=0.5, digits=digits)
+ spin.set_numeric(True)
+ spin.set_snap_to_ticks(True)
+ spin.set_max_length(5)
+ spin.set_width_chars(5)
+ table.attach(spin, col, col + 1, row, row + 1, xoptions=0, yoptions=0)
+ spin.show()
+ adj.connect("value_changed", callback)
+ return spin
+
+ def hscale_in_table(adj, table, row, callback, digits=0, col=1, cols=1):
+ """ Create an hscale and a spinner using the same Adjustment, and set it in table. """
+ scale = gtk.HScale(adj)
+ scale.set_size_request(150, -1)
+ scale.set_digits(digits)
+ scale.set_update_policy(gtk.UPDATE_DISCONTINUOUS)
+ table.attach(scale, col, col + cols, row, row + 1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=0)
+ scale.show()
+
+ spin = gtk.SpinButton(adj, climb_rate=0.5, digits=digits)
+ spin.set_numeric(True)
+ spin.set_snap_to_ticks(True)
+ spin.set_max_length(5)
+ spin.set_width_chars(5)
+ table.attach(spin, col + cols , col + cols + 1, row, row + 1, xoptions=0, yoptions=0)
+ spin.show()
+
+ adj.connect("value_changed", callback)
+
+ return self.MyScale(scale, spin)
+
+ def rotation_in_table(val, table, row, callback):
+ adj = gtk.Adjustment(val, -180.0, 180.0, 1.0)
+ myscale = hscale_in_table(adj, table, row, callback, digits=1)
+ myscale.scale.add_mark(0.0, gtk.POS_BOTTOM, None)
+ myscale.spin.set_max_length(6)
+ myscale.spin.set_width_chars(6)
+ return adj, myscale
+
+ def set_combo_in_table(txt_list, table, row, callback):
+ combo = gtk.combo_box_new_text()
+ for txt in txt_list:
+ combo.append_text(txt)
+ table.attach(combo, 1, 2, row, row + 1, xoptions=gtk.FILL, yoptions=0)
+ combo.show()
+ combo.connect("changed", callback)
+ return combo
+
+ # Return table which is at the top of the dialog, and has several major input widgets.
+ def top_table():
+
+ # Add table for displaying attributes, each having a label and an input widget.
+ table = create_table(2, 3, 10)
+
+ # Curve type
+ row = 0
+ label_in_table(_("Curve Type"), table, row,
+ _("An Epitrochoid pattern is when the moving gear is on the outside of the fixed gear."))
+ self.curve_type_combo = set_combo_in_table([ct.name for ct in curve_types], table, row,
+ self.curve_type_changed)
+
+ row += 1
+ label_in_table(_("Tool"), table, row,
+ _("The tool with which to draw the pattern. "
+ "The Preview tool just draws quickly."))
+ self.tool_combo = set_combo_in_table([tool.name for tool in tools], table, row,
+ self.tool_combo_changed)
+
+ self.long_gradient_checkbox = gtk.CheckButton(_("Long Gradient"))
+ self.long_gradient_checkbox.set_tooltip_text(
+ _("When unchecked, the current tool settings will be used. "
+ "When checked, will use a long gradient to match the length of the pattern, "
+ "based on current gradient and repeat mode from the gradient tool settings.")
+ )
+ self.long_gradient_checkbox.set_border_width(0)
+ table.attach(self.long_gradient_checkbox, 2, 3, row, row + 1, xoptions=0, yoptions=0)
+ self.long_gradient_checkbox.show()
+ self.long_gradient_checkbox.connect("toggled", self.long_gradient_changed)
+
+ return table
+
+ def pattern_notation_frame():
+
+ vbox = gtk.VBox(spacing=0, homogeneous=False)
+
+ add_vertical_space(vbox, 14)
+
+ hbox = gtk.HBox(spacing=5)
+ hbox.set_border_width(5)
+
+ label = gtk.Label(_("Specify pattern using one of the following tabs:"))
+ label.set_tooltip_text(_(
+ "The pattern is specified only by the active tab. Toy Kit is similar to Gears, "
+ "but it uses gears and hole numbers which are found in toy kits. "
+ "If you follow the instructions from the toy kit manuals, results should be similar."))
+ hbox.pack_start(label)
+ label.show()
+
+ alignment = gtk.Alignment(xalign=0.0, yalign=0.0, xscale=0.0, yscale=0.0)
+ alignment.add(hbox)
+ hbox.show()
+ vbox.add(alignment)
+ alignment.show()
+
+ self.pattern_notebook = gtk.Notebook()
+ self.pattern_notebook.set_border_width(0)
+ self.pattern_notebook.connect('switch-page', self.pattern_notation_tab_changed)
+
+ # "Gear" pattern notation.
+
+ # Add table for displaying attributes, each having a label and an input widget.
+ gear_table = create_table(3, 3, 5)
+
+ # Teeth
+ row = 0
+ fixed_gear_tooltip = _(
+ "Number of teeth of fixed gear. The size of the fixed gear is "
+ "proportional to the number of teeth."
+ )
+ label_in_table(_("Fixed Gear Teeth"), gear_table, row, fixed_gear_tooltip)
+ self.outer_teeth_adj = gtk.Adjustment(self.p.outer_teeth, 10, 180, 1)
+ hscale_in_table(self.outer_teeth_adj, gear_table, row, self.outer_teeth_changed)
+
+ row += 1
+ moving_gear_tooltip = _(
+ "Number of teeth of moving gear. The size of the moving gear is "
+ "proportional to the number of teeth."
+ )
+ label_in_table(_("Moving Gear Teeth"), gear_table, row, moving_gear_tooltip)
+ self.inner_teeth_adj = gtk.Adjustment(self.p.inner_teeth, 2, 100, 1)
+ hscale_in_table(self.inner_teeth_adj, gear_table, row, self.inner_teeth_changed)
+
+ row += 1
+ label_in_table(_("Hole percent"), gear_table, row,
+ _("How far is the hole from the center of the moving gear. "
+ "100% means that the hole is at the gear's edge."))
+ self.hole_percent_adj = gtk.Adjustment(self.p.hole_percent, 2.5, 100.0, 0.5)
+ self.hole_percent_myscale = hscale_in_table(self.hole_percent_adj, gear_table,
+ row, self.hole_percent_changed, digits=1)
+
+ # "Kit" pattern notation.
+
+ kit_table = create_table(3, 3, 5)
+
+ row = 0
+ label_in_table(_("Fixed Gear Teeth"), kit_table, row, fixed_gear_tooltip)
+ self.kit_outer_teeth_combo = set_combo_in_table([str(t) for t in ring_teeth], kit_table, row,
+ self.kit_outer_teeth_combo_changed)
+
+ row += 1
+ label_in_table(_("Moving Gear Teeth"), kit_table, row, moving_gear_tooltip)
+ self.kit_inner_teeth_combo = set_combo_in_table([str(t) for t in wheel_teeth], kit_table, row,
+ self.kit_inner_teeth_combo_changed)
+
+ row += 1
+ label_in_table(_("Hole Number"), kit_table, row,
+ _("Hole #1 is at the edge of the gear. "
+ "The maximum hole number is near the center. "
+ "The maximum hole number is different for each gear."))
+ self.kit_hole_adj = gtk.Adjustment(self.p.hole_number, 1, self.p.kit_max_hole_number(), 1)
+ self.kit_hole_myscale = hscale_in_table(self.kit_hole_adj, kit_table, row, self.kit_hole_changed)
+
+ # "Visual" pattern notation.
+
+ visual_table = create_table(3, 5, 5)
+
+ row = 0
+ label_in_table(_("Flower Petals"), visual_table, row, _("The number of petals in the pattern."))
+ self.petals_adj = gtk.Adjustment(self.p.petals, 2, 100, 1)
+ hscale_in_table(self.petals_adj, visual_table, row, self.petals_changed, cols=3)
+
+ row += 1
+ label_in_table(_("Petal Skip"), visual_table, row,
+ _("The number of petals to advance for drawing the next petal."))
+ self.petal_skip_adj = gtk.Adjustment(self.p.petal_skip, 1, 50, 1)
+ hscale_in_table(self.petal_skip_adj, visual_table, row, self.petal_skip_changed, cols=3)
+
+ row += 1
+ label_in_table(_("Hole Radius(%)"), visual_table, row,
+ _("The radius of the hole in the center of the pattern "
+ "where nothing will be drawn. Given as a percentage of the "
+ "size of the pattern. A value of 0 will produce no hole. "
+ "A Value of 99 will produce a thin line on the edge."))
+ self.doughnut_hole_adj = gtk.Adjustment(self.p.doughnut_hole, 0.0, 99.0, 0.1)
+ self.doughnut_hole_myscale = spin_in_table(self.doughnut_hole_adj,
+ visual_table, row, self.doughnut_hole_changed, 1, 1)
+
+ self.doughnut = DoughnutWidget()
+ visual_table.attach(self.doughnut, 2, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=0)
+ self.doughnut.connect('values_changed', self.doughnut_changed)
+ self.doughnut.show()
+
+ label_in_table(_("Width(%)"), visual_table, row,
+ _("The width of the pattern as a percentage of the "
+ "size of the pattern. A Value of 1 will just draw a thin pattern. "
+ "A Value of 100 will fill the entire fixed gear."), 3)
+ self.doughnut_width_adj = gtk.Adjustment(self.p.doughnut_width, 1.0, 100.0, 0.1)
+ self.doughnut_width_myscale = spin_in_table(self.doughnut_width_adj,
+ visual_table, row, self.doughnut_width_changed, 1, 4)
+
+ # Add tables as children of the pattern notebook
+
+ pattern_notation_page[VISUAL_NOTATION] = self.pattern_notebook.append_page(visual_table)
+ self.pattern_notebook.set_tab_label_text(visual_table, _("Visual"))
+ self.pattern_notebook.set_tab_label_packing(visual_table, 0, 0, gtk.PACK_END)
+ visual_table.show()
+
+ pattern_notation_page[TOY_KIT_NOTATION] = self.pattern_notebook.append_page(kit_table)
+ self.pattern_notebook.set_tab_label_text(kit_table, _("Toy Kit"))
+ self.pattern_notebook.set_tab_label_packing(kit_table, 0, 0, gtk.PACK_END)
+ kit_table.show()
+
+ pattern_notation_page[GEAR_NOTATION] = self.pattern_notebook.append_page(gear_table)
+ self.pattern_notebook.set_tab_label_text(gear_table, _("Gears"))
+ self.pattern_notebook.set_tab_label_packing(gear_table, 0, 0, gtk.PACK_END)
+ gear_table.show()
+
+ add_to_box(vbox, self.pattern_notebook)
+
+ add_vertical_space(vbox, 14)
+
+ hbox = gtk.HBox(spacing=5)
+ pattern_table = create_table(1, 3, 5)
+
+ row = 0
+ label_in_table(_("Rotation"), pattern_table, row,
+ _("Rotation of the pattern, in degrees. "
+ "The starting position of the moving gear in the fixed gear."))
+ self.pattern_rotation_adj, myscale = rotation_in_table(
+ self.p.pattern_rotation, pattern_table, row, self.pattern_rotation_changed
+ )
+
+ hbox.pack_end(pattern_table, expand=True, fill=True, padding=0)
+ pattern_table.show()
+
+ vbox.add(hbox)
+ hbox.show()
+
+ return vbox
+
+ def fixed_gear_page():
+
+ vbox = gtk.VBox(spacing=0, homogeneous=False)
+
+ add_vertical_space(vbox, 14)
+
+ table = create_table(4, 2, 10)
+
+ row = 0
+ label_in_table(_("Shape"), table, row,
+ _("The shape of the fixed gear to be used inside current selection. "
+ "Rack is a long round-edged shape provided in the toy kits. "
+ "Frame hugs the boundaries of the rectangular selection, "
+ "use hole=100 in Gear notation to touch boundary. "
+ "Selection will hug boundaries of current selection - try something non-rectangular."))
+ self.shape_combo = set_combo_in_table([shape.name for shape in shapes], table, row,
+ self.shape_combo_changed)
+
+ row += 1
+ label_in_table(_("Sides"), table, row, _("Number of sides of the shape."))
+ self.sides_adj = gtk.Adjustment(self.p.sides, 3, 16, 1)
+ self.sides_myscale = hscale_in_table(self.sides_adj, table, row, self.sides_changed)
+
+ row += 1
+ label_in_table(_("Morph"), table, row, _("Morph fixed gear shape. Only affects some of the shapes."))
+ self.morph_adj = gtk.Adjustment(self.p.morph, 0.0, 1.0, 0.01)
+ self.morph_myscale = hscale_in_table(self.morph_adj, table, row, self.morph_changed, digits=2)
+
+ row += 1
+ label_in_table(_("Rotation"), table, row, _("Rotation of the fixed gear, in degrees"))
+ self.shape_rotation_adj, self.shape_rotation_myscale = rotation_in_table(
+ self.p.shape_rotation, table, row, self.shape_rotation_changed
+ )
+
+ add_to_box(vbox, table)
+ return vbox
+
+ def size_page():
+
+ vbox = gtk.VBox(spacing=0, homogeneous=False)
+ add_vertical_space(vbox, 14)
+ table = create_table(2, 2, 10)
+
+ row = 0
+ label_in_table(_("Margin (px)"), table, row, _("Margin from edge of selection."))
+ self.margin_adj = gtk.Adjustment(self.p.margin_pixels, 0, max(img.height, img.width), 1)
+ hscale_in_table(self.margin_adj, table, row, self.margin_changed)
+
+ row += 1
+ self.equal_w_h_checkbox = gtk.CheckButton(_("Make width and height equal"))
+ self.equal_w_h_checkbox.set_tooltip_text(
+ _("When unchecked, the pattern will fill the current image or selection. "
+ "When checked, the pattern will have same width and height, and will be centered.")
+ )
+ self.equal_w_h_checkbox.set_border_width(15)
+ table.attach(self.equal_w_h_checkbox, 0, 2, row, row + 1)
+ self.equal_w_h_checkbox.show()
+ self.equal_w_h_checkbox.connect("toggled", self.equal_w_h_checkbox_changed)
+
+
+ add_to_box(vbox, table)
+ return vbox
+
+ def add_button_to_box(box, text, callback, tooltip_text=None):
+ btn = gtk.Button(text)
+ if tooltip_text:
+ btn.set_tooltip_text(tooltip_text)
+ box.add(btn)
+ btn.show()
+ btn.connect("clicked", callback)
+ return btn
+
+ def dialog_button_box():
+ hbox = gtk.HBox(homogeneous=True, spacing=20)
+
+ add_button_to_box(hbox, _("Re_draw"), self.redraw,
+ _("If you change the settings of a tool, change color, or change the selection, "
+ "press this to preview how the pattern looks."))
+ add_button_to_box(hbox, _("_Reset"), self.reset_params)
+ add_button_to_box(hbox, _("_Cancel"), self.cancel_window)
+ self.ok_btn = add_button_to_box(hbox, _("_OK"), self.ok_window)
+
+ self.save_option_combo = gtk.combo_box_new_text()
+ for txt in save_options:
+ self.save_option_combo.append_text(txt)
+ self.save_option_combo.set_tooltip_text(
+ _("Choose whether to save as new layer, redraw on last active layer, or save to path")
+ )
+ hbox.add(self.save_option_combo)
+ self.save_option_combo.show()
+ self.save_option_combo.connect("changed", self.save_option_changed)
+
+ return hbox
+
+ def create_ui():
+
+ # Create the dialog
+ gtk.Window.__init__(self)
+ self.set_title(_("Spyrogimp"))
+ self.set_default_size(350, -1)
+ self.set_border_width(10)
+ # self.set_keep_above(True) # keep the window on top
+
+ # Vertical box in which we will add all the UI elements.
+ vbox = gtk.VBox(spacing=10, homogeneous=False)
+ self.add(vbox)
+
+ box = gimpui.HintBox(_("Draw spyrographs using current tool settings and selection."))
+ vbox.pack_start(box, expand=False)
+ box.show()
+
+ add_horizontal_separator(vbox)
+
+ add_to_box(vbox, top_table())
+
+ self.main_notebook = gtk.Notebook()
+ self.main_notebook.set_show_tabs(True)
+ self.main_notebook.set_border_width(5)
+
+ pattern_frame = pattern_notation_frame()
+ self.main_notebook.append_page(pattern_frame, gtk.Label(_("Curve Pattern")))
+ pattern_frame.show()
+ fixed_g_page = fixed_gear_page()
+ self.main_notebook.append_page(fixed_g_page, gtk.Label(_("Fixed Gear")))
+ fixed_g_page.show()
+ size_p = size_page()
+ self.main_notebook.append_page(size_p, gtk.Label(_("Size")))
+ size_p.show()
+
+ vbox.add(self.main_notebook)
+ self.main_notebook.show()
+
+ add_horizontal_separator(vbox)
+
+ self.progress_bar = gtk.ProgressBar() # gimpui.ProgressBar() - causes gimppdbprogress error message.
+ self.progress_bar.set_size_request(-1, 30)
+ vbox.add(self.progress_bar)
+ self.progress_bar.show()
+
+ add_to_box(vbox, dialog_button_box())
+
+ vbox.show()
+ self.show()
+
+ self.enable_incremental_drawing = False
+
+ self.img = img
+ # Remember active layer, so we can restore it when the plugin is done.
+ self.active_layer = layer
+
+ self.p = unshelf_parameters() # Model
+
+ self.engine = DrawingEngine(img, self.p)
+
+ # Make a new GIMP layer to draw on
+ self.spyro_layer = gimp.Layer(img, layer_name, img.width, img.height,
+ layer.type_with_alpha, 100, NORMAL_MODE)
+ img.add_layer(self.spyro_layer, 0)
+
+ self.drawing_layer = self.spyro_layer
+
+ gimpui.gimp_ui_init()
+ create_ui()
+ self.update_view()
+
+ # Obey the window manager quit signal
+ self.connect("destroy", self.cancel_window)
+ # Connect Escape key to quit the window as well.
+ self.connect('myescape', self.cancel_window)
+
+ # Setup for Handling incremental/interactive drawing of pattern
+ self.idle_task = None
+ self.enable_incremental_drawing = True
+
+ # Draw pattern of the current settings.
+ self.start_new_incremental_drawing()
+
+ # Callbacks for closing the plugin
+
+ def clear_idle_task(self):
+ if self.idle_task:
+ gobject.source_remove(self.idle_task)
+ # Close the undo group in the likely case the idle task left it open.
+ self.img.undo_group_end()
+ self.idle_task = None
+
+ def ok_window(self, widget):
+ """ Called when clicking on the 'close' button. """
+
+ self.ok_btn.set_sensitive(False)
+
+ shelf_parameters(self.p)
+
+ if self.p.save_option == SAVE_AS_NEW_LAYER:
+ if self.spyro_layer in self.img.layers:
+ self.img.active_layer = self.spyro_layer
+
+ # If we are in the middle of incremental draw, we want to complete it, and only then to exit.
+ # However, in order to complete it, we need to create another idle task.
+ if self.idle_task:
+ def quit_dialog_on_completion():
+ while self.idle_task:
+ yield True
+
+ gtk.main_quit() # This will quit the dialog.
+ yield False
+
+ task = quit_dialog_on_completion()
+ gobject.idle_add(task.next)
+ else:
+ gtk.main_quit()
+ else:
+ # If there is an incremental drawing taking place, lets stop it.
+ self.clear_idle_task()
+
+ if self.spyro_layer in self.img.layers:
+ self.img.remove_layer(self.spyro_layer)
+ self.img.active_layer = self.active_layer
+
+ self.drawing_layer = self.active_layer
+
+ def draw_full(tool):
+ self.progress_start()
+ yield True
+
+ self.engine.reset_incremental()
+
+ self.img.undo_group_start()
+
+ while self.engine.has_more_strokes():
+ yield True
+ self.draw_next_chunk(tool=tool)
+
+ self.img.undo_group_end()
+
+ pdb.gimp_displays_flush()
+
+ gtk.main_quit()
+ yield False
+
+ tool = SaveToPathTool(self.img) if self.p.save_option == SAVE_AS_PATH else None
+ task = draw_full(tool)
+ gobject.idle_add(task.next)
+
+ def cancel_window(self, widget, what=None):
+ self.clear_idle_task()
+
+ # We want to delete the temporary layer, but as a precaution, lets ask first,
+ # maybe it was already deleted by the user.
+ if self.spyro_layer in self.img.layers:
+ self.img.remove_layer(self.spyro_layer)
+ pdb.gimp_displays_flush()
+ gtk.main_quit()
+
+ def update_view(self):
+ """ Update the UI to reflect the values in the Pattern Parameters. """
+ self.curve_type_combo.set_active(self.p.curve_type)
+ self.curve_type_side_effects()
+
+ self.pattern_notebook.set_current_page(pattern_notation_page[self.p.pattern_notation])
+
+ self.outer_teeth_adj.set_value(self.p.outer_teeth)
+ self.inner_teeth_adj.set_value(self.p.inner_teeth)
+ self.hole_percent_adj.set_value(self.p.hole_percent)
+ self.pattern_rotation_adj.set_value(self.p.pattern_rotation)
+
+ self.kit_outer_teeth_combo.set_active(self.p.kit_fixed_gear_index)
+ self.kit_inner_teeth_combo.set_active(self.p.kit_moving_gear_index)
+ self.kit_hole_adj.set_value(self.p.hole_number)
+ self.kit_inner_teeth_combo_side_effects()
+
+ self.petals_adj.set_value(self.p.petals)
+ self.petal_skip_adj.set_value(self.p.petal_skip)
+ self.doughnut_hole_adj.set_value(self.p.doughnut_hole)
+ self.doughnut.set_hole_radius(self.p.doughnut_hole)
+ self.doughnut_width_adj.set_value(self.p.doughnut_width)
+ self.doughnut.set_width(self.p.doughnut_width)
+ self.petals_changed_side_effects()
+
+ self.shape_combo.set_active(self.p.shape_index)
+ self.shape_combo_side_effects()
+ self.sides_adj.set_value(self.p.sides)
+ self.morph_adj.set_value(self.p.morph)
+ self.equal_w_h_checkbox.set_active(self.p.equal_w_h)
+ self.shape_rotation_adj.set_value(self.p.shape_rotation)
+
+ self.margin_adj.set_value(self.p.margin_pixels)
+ self.tool_combo.set_active(self.p.tool_index)
+ self.long_gradient_checkbox.set_active(self.p.long_gradient)
+ self.save_option_combo.set_active(self.p.save_option)
+
+ def reset_params(self, widget):
+ self.engine.p = self.p = PatternParameters()
+ self.update_view()
+
+ # Callbacks to handle changes in dialog parameters.
+
+ def curve_type_side_effects(self):
+ if curve_types[self.p.curve_type].supports_shapes():
+ self.shape_combo.set_sensitive(True)
+
+ self.sides_myscale.set_sensitive(shapes[self.p.shape_index].has_sides())
+ self.morph_myscale.set_sensitive(shapes[self.p.shape_index].can_morph())
+ self.shape_rotation_myscale.set_sensitive(shapes[self.p.shape_index].can_rotate())
+
+ self.hole_percent_myscale.set_sensitive(True)
+ self.kit_hole_myscale.set_sensitive(True)
+
+ self.doughnut_hole_myscale.set_sensitive(True)
+ self.doughnut_width_myscale.set_sensitive(True)
+ else:
+ # Lissajous curves do not have shapes, or holes for moving gear
+ self.shape_combo.set_sensitive(False)
+
+ self.sides_myscale.set_sensitive(False)
+ self.morph_myscale.set_sensitive(False)
+ self.shape_rotation_myscale.set_sensitive(False)
+
+ self.hole_percent_myscale.set_sensitive(False)
+ self.kit_hole_myscale.set_sensitive(False)
+
+ self.doughnut_hole_myscale.set_sensitive(False)
+ self.doughnut_width_myscale.set_sensitive(False)
+
+ def curve_type_changed(self, val):
+ self.p.curve_type = val.get_active()
+ self.curve_type_side_effects()
+ self.redraw()
+
+ def pattern_notation_tab_changed(self, notebook, page, page_num, user_param1=None):
+ if self.enable_incremental_drawing:
+ for notation in pattern_notation_page:
+ if pattern_notation_page[notation] == page_num:
+ self.p.pattern_notation = notation
+
+ self.redraw()
+
+ # Callbacks: pattern changes using the Toy Kit notation.
+
+ def kit_outer_teeth_combo_changed(self, val):
+ self.p.kit_fixed_gear_index = val.get_active()
+ self.redraw()
+
+ def kit_inner_teeth_combo_side_effects(self):
+ # Change the max hole number according to the newly activated wheel.
+ # We might also need to update the hole value, if it is larger than the new max.
+ max_hole_number = self.p.kit_max_hole_number()
+ if self.p.hole_number > max_hole_number:
+ self.p.hole_number = max_hole_number
+ self.kit_hole_adj.set_value(max_hole_number)
+ self.kit_hole_adj.set_upper(max_hole_number)
+
+ def kit_inner_teeth_combo_changed(self, val):
+ self.p.kit_moving_gear_index = val.get_active()
+ self.kit_inner_teeth_combo_side_effects()
+ self.redraw()
+
+ def kit_hole_changed(self, val):
+ self.p.hole_number = val.value
+ self.redraw()
+
+ # Callbacks: pattern changes using the Gears notation.
+
+ def outer_teeth_changed(self, val):
+ self.p.outer_teeth = val.value
+ self.redraw()
+
+ def inner_teeth_changed(self, val):
+ self.p.inner_teeth = val.value
+ self.redraw()
+
+ def hole_percent_changed(self, val):
+ self.p.hole_percent = val.value
+ self.redraw()
+
+ def pattern_rotation_changed(self, val):
+ self.p.pattern_rotation = val.value
+ self.redraw()
+
+ # Callbacks: pattern changes using the Visual notation.
+
+ def petals_changed_side_effects(self):
+ max_petal_skip = int(self.p.petals/2)
+ if self.p.petal_skip > max_petal_skip:
+ self.p.petal_skip = max_petal_skip
+ self.petal_skip_adj.set_value(max_petal_skip)
+ self.petal_skip_adj.set_upper(max_petal_skip)
+
+ def petals_changed(self, val):
+ self.p.petals = int(val.value)
+ self.petals_changed_side_effects()
+ self.redraw()
+
+ def petal_skip_changed(self, val):
+ self.p.petal_skip = int(val.value)
+ self.redraw()
+
+ def doughnut_hole_changed(self, val):
+ self.p.doughnut_hole = val.value
+ self.doughnut.set_hole_radius(val.value)
+ self.redraw()
+
+ def doughnut_width_changed(self, val):
+ self.p.doughnut_width = val.value
+ self.doughnut.set_width(val.value)
+ self.redraw()
+
+ def doughnut_changed(self, widget, hole, width):
+ self.doughnut_hole_adj.set_value(hole)
+ self.doughnut_width_adj.set_value(width)
+ # We don't need to redraw, because the callbacks of the doughnut hole and
+ # width spinners will be triggered by the above lines.
+
+ # Callbacks: Fixed gear
+
+ def shape_combo_side_effects(self):
+ self.sides_myscale.set_sensitive(shapes[self.p.shape_index].has_sides())
+ self.morph_myscale.set_sensitive(shapes[self.p.shape_index].can_morph())
+ self.shape_rotation_myscale.set_sensitive(shapes[self.p.shape_index].can_rotate())
+ self.equal_w_h_checkbox.set_sensitive(shapes[self.p.shape_index].can_equal_w_h())
+
+ def shape_combo_changed(self, val):
+ self.p.shape_index = val.get_active()
+ self.shape_combo_side_effects()
+ self.redraw()
+
+ def sides_changed(self, val):
+ self.p.sides = val.value
+ self.redraw()
+
+ def morph_changed(self, val):
+ self.p.morph = val.value
+ self.redraw()
+
+ def equal_w_h_checkbox_changed(self, val):
+ self.p.equal_w_h = val.get_active()
+ self.redraw()
+
+ def shape_rotation_changed(self, val):
+ self.p.shape_rotation = val.value
+ self.redraw()
+
+ def margin_changed(self, val) :
+ self.p.margin_pixels = val.value
+ self.redraw()
+
+ # Style callbacks
+
+ def tool_changed_side_effects(self):
+ self.long_gradient_checkbox.set_sensitive(tools[self.p.tool_index].can_color)
+
+ def tool_combo_changed(self, val):
+ self.p.tool_index = val.get_active()
+ self.tool_changed_side_effects()
+ self.redraw()
+
+ def long_gradient_changed(self, val):
+ self.p.long_gradient = val.get_active()
+ self.redraw()
+
+ def save_option_changed(self, val):
+ self.p.save_option = self.save_option_combo.get_active()
+
+ # Progress bar of plugin window.
+
+ def progress_start(self):
+ self.progress_bar.set_text(_("Rendering Pattern"))
+ self.progress_bar.set_fraction(0.0)
+ pdb.gimp_displays_flush()
+
+ def progress_end(self):
+ self.progress_bar.set_text("")
+ self.progress_bar.set_fraction(0.0)
+
+ def progress_update(self):
+ self.progress_bar.set_fraction(self.engine.fraction_done())
+
+ def progress_unknown(self):
+ self.progress_bar.set_text(_("Please wait : Rendering Pattern"))
+ self.progress_bar.pulse()
+ pdb.gimp_displays_flush()
+
+ # Incremental drawing.
+
+ def draw_next_chunk(self, tool=None):
+ """ Incremental drawing """
+
+ t = time.time()
+
+ chunk_size = self.engine.draw_next_chunk(self.drawing_layer, tool=tool)
+
+ draw_time = time.time() - t
+ self.engine.report_time(draw_time)
+ print("Chunk size " + str(chunk_size) + " time " + str(draw_time))
+
+ if self.engine.has_more_strokes():
+ self.progress_update()
+ else:
+ self.progress_end()
+
+ pdb.gimp_displays_flush()
+
+ def start_new_incremental_drawing(self):
+ """
+ Compute strokes for the current pattern, and store then in the IncrementalDraw object,
+ so they can be drawn in pieces without blocking the user.
+ Finally, draw the first chunk of strokes.
+ """
+
+ def incremental_drawing():
+ self.progress_start()
+ yield True
+ self.engine.reset_incremental()
+
+ self.img.undo_group_start()
+ while self.engine.has_more_strokes():
+ yield True
+ self.draw_next_chunk()
+ self.img.undo_group_end()
+
+ self.idle_task = None
+ yield False
+
+ # Start new idle task to perform incremental drawing in the background.
+ self.clear_idle_task()
+ task = incremental_drawing()
+ self.idle_task = gobject.idle_add(task.next)
+
+ def clear(self):
+ """ Clear current drawing. """
+ # pdb.gimp_edit_clear(self.spyro_layer)
+ self.spyro_layer.fill(FILL_TRANSPARENT)
+
+ def redraw(self, data=None):
+ if self.enable_incremental_drawing:
+ self.clear()
+ self.start_new_incremental_drawing()
+
+
+# Bind escape to the new signal we created, named "myescape".
+gobject.type_register(SpyroWindow)
+gtk.binding_entry_add_signal(SpyroWindow, gtk.keysyms.Escape, 0, 'myescape', str, 'escape')
+
+
+class SpyrogimpPlusPlugin(gimpplugin.plugin):
+
+ # Implementation of plugin.
+ def plug_in_spyrogimp(self, run_mode, image, layer,
+ curve_type=0, shape=0, sides=3, morph=0.0,
+ fixed_teeth=96, moving_teeth=36, hole_percent=100.0,
+ margin=0, equal_w_h=0,
+ pattern_rotation=0.0, shape_rotation=0.0,
+ tool=1, long_gradient=False):
+ if run_mode == RUN_NONINTERACTIVE:
+ pp = PatternParameters()
+ pp.curve_type = curve_type
+ pp.shape_index = shape
+ pp.sides = sides
+ pp.morph = morph
+ pp.outer_teeth = fixed_teeth
+ pp.inner_teeth = moving_teeth
+ pp.hole_percent = hole_percent
+ pp.margin_pixels = margin
+ pp.equal_w_h = equal_w_h
+ pp.pattern_rotation = pattern_rotation
+ pp.shape_rotation = shape_rotation
+ pp.tool_index = tool
+ pp.long_gradient = long_gradient
+
+ engine = DrawingEngine(image, pp)
+ engine.draw_full(layer)
+
+ elif run_mode == RUN_INTERACTIVE:
+ window = SpyroWindow(image, layer)
+ gtk.main()
+
+ elif run_mode == RUN_WITH_LAST_VALS:
+ pp = unshelf_parameters()
+ engine = DrawingEngine(image, pp)
+ engine.draw_full(layer)
+
+ def query(self):
+ plugin_name = "plug_in_spyrogimp"
+ label = N_("Spyrogimp...")
+ menu = "<Image>/Filters/Render/"
+
+ params = [
+ # (type, name, description
+ (PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }"),
+ (PDB_IMAGE, "image", "Input image"),
+ (PDB_DRAWABLE, "drawable", "Input drawable"),
+ (PDB_INT32, "curve_type",
+ "The curve type { Spyrograph (0), Epitrochoid (1), Sine (2), Lissajous(3) }"),
+ (PDB_INT32, "shape", "Shape of fixed gear"),
+ (PDB_INT32, "sides", "Number of sides of fixed gear (3 or greater). Only used by some shapes."),
+ (PDB_FLOAT, "morph", "Morph shape of fixed gear, between 0 and 1. Only used by some shapes."),
+ (PDB_INT32, "fixed_teeth", "Number of teeth for fixed gear"),
+ (PDB_INT32, "moving_teeth", "Number of teeth for moving gear"),
+ (PDB_FLOAT, "hole_percent", "Location of hole in moving gear in percent, where 100 means that "
+ "the hole is at the edge of the gear, and 0 means the hole is at the center"),
+ (PDB_INT32, "margin", "Margin from selection, in pixels"),
+ (PDB_INT32, "equal_w_h", "Make height and width equal (TRUE or FALSE)"),
+ (PDB_FLOAT, "pattern_rotation", "Pattern rotation, in degrees"),
+ (PDB_FLOAT, "shape_rotation", "Shape rotation of fixed gear, in degrees"),
+ (PDB_INT32, "tool", "Tool to use for drawing the pattern."),
+ (PDB_INT32, "long_gradient",
+ "Whether to apply a long gradient to match the length of the pattern (TRUE or FALSE). "
+ "Only applicable to some of the tools.")
+ ]
+
+ gimp.domain_register("gimp20-python", gimp.locale_directory)
+
+ gimp.install_procedure(
+ plugin_name,
+ N_("Draw spyrographs using current tool settings and selection."),
+ "Uses current tool settings to draw Spyrograph patterns. "
+ "The size and location of the pattern is based on the current selection.",
+ "Elad Shahar",
+ "Elad Shahar",
+ "2018",
+ label,
+ "*",
+ PLUGIN,
+ params,
+ []
+ )
+
+ gimp.menu_register(plugin_name, menu)
+
+
+if __name__ == '__main__':
+ SpyrogimpPlusPlugin().start()
diff --git a/plug-ins/pygimp/plug-ins/whirlpinch.py b/plug-ins/pygimp/plug-ins/whirlpinch.py
new file mode 100755
index 0000000..6c50e89
--- /dev/null
+++ b/plug-ins/pygimp/plug-ins/whirlpinch.py
@@ -0,0 +1,227 @@
+#!/usr/bin/env python2
+
+# Gimp-Python - allows the writing of Gimp plugins in Python.
+# Copyright (C) 1997 James Henstridge <james@daa.com.au>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+# Algorithms stolen from the whirl and pinch plugin distributed with Gimp,
+# by Federico Mena Quintero and Scott Goehring
+#
+# This version does the same thing, except there is no preview, and it is
+# written in python and is slower.
+
+import math, struct
+from gimpfu import *
+
+class pixel_fetcher:
+ def __init__(self, drawable):
+ self.col = -1
+ self.row = -1
+ self.img_width = drawable.width
+ self.img_height = drawable.height
+ self.img_bpp = drawable.bpp
+ self.img_has_alpha = drawable.has_alpha
+ self.tile_width = gimp.tile_width()
+ self.tile_height = gimp.tile_height()
+ self.bg_colour = '\0\0\0\0'
+ self.bounds = drawable.mask_bounds
+ self.drawable = drawable
+ self.tile = None
+ def set_bg_colour(self, r, g, b, a):
+ self.bg_colour = struct.pack('BBB', r,g,b)
+ if self.img_has_alpha:
+ self.bg_colour = self.bg_colour + chr(a)
+ def get_pixel(self, x, y):
+ sel_x1, sel_y1, sel_x2, sel_y2 = self.bounds
+ if x < sel_x1 or x >= sel_x2 or y < sel_y1 or y >= sel_y2:
+ return self.bg_colour
+ col = x / self.tile_width
+ coloff = x % self.tile_width
+ row = y / self.tile_height
+ rowoff = y % self.tile_height
+
+ if col != self.col or row != self.row or self.tile == None:
+ self.tile = self.drawable.get_tile(False, row, col)
+ self.col = col
+ self.row = row
+ return self.tile[coloff, rowoff]
+
+class Dummy:
+ pass
+
+def whirl_pinch(image, drawable, whirl, pinch, radius):
+ self = Dummy()
+ self.width = drawable.width
+ self.height = drawable.height
+ self.bpp = drawable.bpp
+ self.has_alpha = drawable.has_alpha
+ self.bounds = drawable.mask_bounds
+ self.sel_x1, self.sel_y1, self.sel_x2, self.sel_y2 = \
+ drawable.mask_bounds
+ self.sel_w = self.sel_x2 - self.sel_x1
+ self.sel_h = self.sel_y2 - self.sel_y1
+ self.cen_x = (self.sel_x1 + self.sel_x2 - 1) / 2.0
+ self.cen_y = (self.sel_y1 + self.sel_y2 - 1) / 2.0
+ xhsiz = (self.sel_w - 1) / 2.0
+ yhsiz = (self.sel_h - 1) / 2.0
+
+ if xhsiz < yhsiz:
+ self.scale_x = yhsiz / xhsiz
+ self.scale_y = 1.0
+ elif xhsiz > yhsiz:
+ self.scale_x = 1.0
+ self.scale_y = xhsiz / yhsiz
+ else:
+ self.scale_x = 1.0
+ self.scale_y = 1.0
+
+ self.radius = max(xhsiz, yhsiz);
+
+ if not drawable.is_rgb and not drawable.is_grey:
+ return
+
+ gimp.tile_cache_ntiles(2 * (1 + self.width / gimp.tile_width()))
+
+ whirl = whirl * math.pi / 180
+ dest_rgn = drawable.get_pixel_rgn(self.sel_x1, self.sel_y1,
+ self.sel_w, self.sel_h, True, True)
+ pft = pixel_fetcher(drawable)
+ pfb = pixel_fetcher(drawable)
+
+ bg_colour = gimp.get_background()
+
+ pft.set_bg_colour(bg_colour[0], bg_colour[1], bg_colour[2], 0)
+ pfb.set_bg_colour(bg_colour[0], bg_colour[1], bg_colour[2], 0)
+
+ progress = 0
+ max_progress = self.sel_w * self.sel_h
+
+ gimp.progress_init("Whirling and pinching")
+
+ self.radius2 = self.radius * self.radius * radius
+ pixel = ['', '', '', '']
+ values = [0,0,0,0]
+
+ for row in range(self.sel_y1, (self.sel_y1+self.sel_y2)/2+1):
+ top_p = ''
+ bot_p = ''
+ for col in range(self.sel_x1, self.sel_x2):
+ q, cx, cy = calc_undistorted_coords(self, col,
+ row, whirl, pinch,
+ radius)
+ if q:
+ if cx >= 0: ix = int(cx)
+ else: ix = -(int(-cx) + 1)
+ if cy >= 0: iy = int(cy)
+ else: iy = -(int(-cy) + 1)
+ pixel[0] = pft.get_pixel(ix, iy)
+ pixel[1] = pft.get_pixel(ix+1, iy)
+ pixel[2] = pft.get_pixel(ix, iy+1)
+ pixel[3] = pft.get_pixel(ix+1, iy+1)
+ for i in range(self.bpp):
+ values[0] = ord(pixel[0][i])
+ values[1] = ord(pixel[1][i])
+ values[2] = ord(pixel[2][i])
+ values[3] = ord(pixel[3][i])
+ top_p = top_p + bilinear(cx,cy, values)
+ cx = self.cen_x + (self.cen_x - cx)
+ cy = self.cen_y + (self.cen_y - cy)
+ if cx >= 0: ix = int(cx)
+ else: ix = -(int(-cx) + 1)
+ if cy >= 0: iy = int(cy)
+ else: iy = -(int(-cy) + 1)
+ pixel[0] = pfb.get_pixel(ix, iy)
+ pixel[1] = pfb.get_pixel(ix+1, iy)
+ pixel[2] = pfb.get_pixel(ix, iy+1)
+ pixel[3] = pfb.get_pixel(ix+1, iy+1)
+ tmp = ''
+ for i in range(self.bpp):
+ values[0] = ord(pixel[0][i])
+ values[1] = ord(pixel[1][i])
+ values[2] = ord(pixel[2][i])
+ values[3] = ord(pixel[3][i])
+ tmp = tmp + bilinear(cx,cy, values)
+ bot_p = tmp + bot_p
+ else:
+ top_p = top_p + pft.get_pixel(col, row)
+ bot_p = pfb.get_pixel((self.sel_x2 - 1) -
+ (col - self.sel_x1), (self.sel_y2-1) -
+ (row - self.sel_y1)) + bot_p
+
+ dest_rgn[self.sel_x1:self.sel_x2, row] = top_p
+ dest_rgn[self.sel_x1:self.sel_x2, (self.sel_y2 - 1)
+ - (row - self.sel_y1)] = bot_p
+
+ progress = progress + self.sel_w * 2
+ gimp.progress_update(float(progress) / max_progress)
+
+ drawable.flush()
+ drawable.merge_shadow(True)
+ drawable.update(self.sel_x1,self.sel_y1,self.sel_w,self.sel_h)
+
+def calc_undistorted_coords(self, wx, wy, whirl, pinch, radius):
+ dx = (wx - self.cen_x) * self.scale_x
+ dy = (wy - self.cen_y) * self.scale_y
+ d = dx * dx + dy * dy
+ inside = d < self.radius2
+
+ if inside:
+ dist = math.sqrt(d / radius) / self.radius
+ if (d == 0.0):
+ factor = 1.0
+ else:
+ factor = math.pow(math.sin(math.pi / 2 * dist),
+ -pinch)
+ dx = dx * factor
+ dy = dy * factor
+ factor = 1 - dist
+ ang = whirl * factor * factor
+ sina = math.sin(ang)
+ cosa = math.cos(ang)
+ x = (cosa * dx - sina * dy) / self.scale_x + self.cen_x
+ y = (sina * dx + cosa * dy) / self.scale_y + self.cen_y
+ else:
+ x = wx
+ y = wy
+ return inside, float(x), float(y)
+
+def bilinear(x, y, values):
+ x = x % 1.0
+ y = y % 1.0
+ m0 = values[0] + x * (values[1] - values[0])
+ m1 = values[2] + x * (values[3] - values[2])
+ return chr(int(m0 + y * (m1 - m0)))
+
+
+register(
+ "python-fu-whirl-pinch",
+ "Distorts an image by whirling and pinching",
+ "Distorts an image by whirling and pinching",
+ "James Henstridge (translated from C plugin)",
+ "James Henstridge",
+ "1997-1999",
+ "_Whirl and Pinch...",
+ "RGB*, GRAY*",
+ [
+ (PF_IMAGE, "image", "Input image", None),
+ (PF_DRAWABLE, "drawable", "Input drawable", None),
+ (PF_SLIDER, "whirl", "Whirl angle", 90, (-360, 360, 1)),
+ (PF_FLOAT, "pinch", "Pinch amount", 0),
+ (PF_FLOAT, "radius", "radius", 1)
+ ],
+ [],
+ whirl_pinch, menu="<Image>/Filters/Distorts")
+
+main()