diff options
Diffstat (limited to 'plug-ins/pygimp/plug-ins')
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 = { + '&': '&', + '<': '<', + '>': '>', + '"': '"' +} + +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() |