Initial checkin of Pika from heckimp

This commit is contained in:
2023-09-25 15:35:21 -07:00
commit 891e999216
6761 changed files with 5240685 additions and 0 deletions

View File

@ -0,0 +1,337 @@
#!/usr/bin/env python3
# Pika-Python - allows the writing of Pika 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 os.path
import sys
import gi
gi.require_version('Pika', '3.0')
from gi.repository import Pika
gi.require_version('PikaUi', '3.0')
from gi.repository import PikaUi
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
def N_(message): return message
def _(message): return GLib.dgettext(None, message)
escape_table = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;'
}
style_def = """body {
width: 100%%;
font-size: %dpx;
background-color: #000000;
color: #ffffff;
}
"""
preamble = """<!DOCTYPE html>
<html>
<head>
<title>CSS Color XHTML written by PIKA</title>
%s
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<pre>
"""
postamble = """\n</pre>\n</body>\n</html>\n"""
fmt_from_bpp = {
3: 'BBB',
6: 'HHH',
12: 'III'
}
def save_colorxhtml(procedure, run_mode, image, n_layers, layers, file, args, data):
source_file = args.index(0)
characters = args.index(1)
size = args.index(2)
separate = args.index(3)
if file is None:
error = 'No file given'
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
GLib.Error(error))
if run_mode == Pika.RunMode.INTERACTIVE:
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
PikaUi.init ("file-colorxhtml-save")
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
dialog = Gtk.Dialog(use_header_bar=use_header_bar,
title=_("Save as colored HTML text..."))
dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
dialog.add_button(_("_OK"), Gtk.ResponseType.OK)
choose_file_dialog = Gtk.FileChooserDialog(use_header_bar=use_header_bar,
title=_("Read characters from file..."),
action=Gtk.FileChooserAction.OPEN)
choose_file_dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
choose_file_dialog.add_button(_("_OK"), Gtk.ResponseType.OK)
def choose_file(button, user_data=None):
choose_file_dialog.show()
if choose_file_dialog.run() == Gtk.ResponseType.OK:
characters_entry.set_text(choose_file_dialog.get_filename())
choose_file_dialog.hide()
grid = Gtk.Grid()
grid.set_column_homogeneous(False)
grid.set_border_width(10)
grid.set_column_spacing(10)
grid.set_row_spacing(10)
row = 0
label = Gtk.Label(label=_("Characters"))
label.set_tooltip_text(_("Characters that will be used as colored pixels. "))
grid.attach(label, 0, row, 1 , 1)
label.show()
characters_entry = Gtk.Entry()
characters_entry.set_width_chars(20)
characters_entry.set_max_width_chars(80)
characters_entry.set_text(characters)
characters_entry.set_placeholder_text(_("Characters or file location"))
grid.attach(characters_entry, 1, row, 1, 1)
characters_entry.show()
row += 1
characters_checkbox = Gtk.CheckButton(label=_("Read characters from file"))
characters_checkbox.set_active(source_file)
characters_checkbox.set_tooltip_text(
_("If set, the Characters text entry will be used as a file name, "
"from which the characters will be read. Otherwise, the characters "
"in the text entry will be used to render the image."))
grid.attach(characters_checkbox, 0, row, 1, 1)
characters_checkbox.show()
choose_file_button = Gtk.Button(label=_("Choose file"))
grid.attach(choose_file_button, 1, row, 1, 1)
choose_file_button.connect("clicked", choose_file)
choose_file_button.show()
row += 1
label = Gtk.Label(label=_("Font Size(px)"))
grid.attach(label, 0, row, 1 , 1)
label.show()
font_size_adj = Gtk.Adjustment.new(size, 0.0, 100.0, 1.0, 0.0, 0.0)
font_size_spin = Gtk.SpinButton.new(font_size_adj, climb_rate=1.0, digits=0)
font_size_spin.set_numeric(True)
grid.attach(font_size_spin, 1, row, 1 , 1)
font_size_spin.show()
row += 1
separate_checkbox = Gtk.CheckButton(label=_("Write separate CSS file"))
separate_checkbox.set_active(separate)
grid.attach(separate_checkbox, 0, row, 2, 1)
separate_checkbox.show()
dialog.get_content_area().add(grid)
grid.show()
dialog.show()
if dialog.run() == Gtk.ResponseType.OK:
separate = separate_checkbox.get_active()
size = font_size_spin.get_value_as_int()
source_file = characters_checkbox.get_active()
characters = characters_entry.get_text()
else:
return procedure.new_return_values(Pika.PDBStatusType.CANCEL,
GLib.Error())
#For now, work with a single layer
layer = layers[0]
width = layer.get_width()
height = layer.get_height()
bpp = layer.get_bpp()
html = open(file.peek_path(), 'w')
if separate:
dirname, cssfile = os.path.split(file.peek_path())
cssfile = os.path.splitext(cssfile)[0] + '.css'
cssname = os.path.join(dirname, cssfile)
css = open(cssname, 'w')
if source_file:
characters_file = open(characters, 'r')
chars = characters_file.read()
characters_file.close()
else:
chars = characters
# Remove unprintable characters from "chars".
# TODO: This only handles ascii files. It would be nice to handle unicode
# files, so this could work for any language.
goodchars = string.digits + string.ascii_letters + string.punctuation
badchars = ''.join(chr(i) for i in range(256) if chr(i) not in goodchars)
allchars = str.maketrans('', '', badchars)
chars = chars.translate(allchars)
data = [escape_table.get(c, c) for c in chars]
if data:
data.reverse()
else:
data = list('X' * 80)
Pika.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 = []
# Constants used for formatting the pixel color. We can handle image
# types where each color is 8 bits, 16 bits, or 32 bit integers.
fmt = fmt_from_bpp[bpp]
pixel_shift = 8 * (bpp//3 - 1)
for y in range(0, height):
# The characters in "chars" will be used to draw the next row.
# Lets fill "chars" with data so it has at least enough characters.
while len(chars) < width:
chars[0:0] = data
for x in range(0, width):
pixel_bytes = layer.get_pixel(x, y)
pixel_tuple = struct.unpack(fmt, pixel_bytes)
if bpp > 3:
pixel_tuple=(
pixel_tuple[0] >> pixel_shift,
pixel_tuple[1] >> pixel_shift,
pixel_tuple[2] >> pixel_shift,
)
color = '%02x%02x%02x' % pixel_tuple
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')
Pika.progress_update(y / float(height))
html.write(postamble)
html.close()
if separate:
css.close()
return Pika.ValueArray.new_from_values([
GObject.Value(Pika.PDBStatusType, Pika.PDBStatusType.SUCCESS)
])
class ColorXhtml(Pika.PlugIn):
## Parameters ##
__gproperties__ = {
"source-file":(bool,
_("_Read characters from file, if true, or use text entry"),
_("_Read characters from file, if true, or use text entry"),
False,
GObject.ParamFlags.READWRITE),
"characters": (str,
_("_File to read or characters to use"),
_("_File to read or characters to use"),
"foo",
GObject.ParamFlags.READWRITE),
"font-size": (int,
_("Fo_nt size in pixels"),
_("Fo_nt size in pixels"),
5, 100, 10,
GObject.ParamFlags.READWRITE),
"separate": (bool,
_("_Write a separate CSS file"),
_("_Write a separate CSS file"),
False,
GObject.ParamFlags.READWRITE)
}
## PikaPlugIn virtual methods ##
def do_set_i18n(self, procname):
return True, 'pika30-python', None
def do_query_procedures(self):
return [ 'file-colorxhtml-save' ]
def do_create_procedure(self, name):
procedure = None
if name == 'file-colorxhtml-save':
procedure = Pika.SaveProcedure.new(self, name,
Pika.PDBProcType.PLUGIN,
save_colorxhtml, None)
procedure.set_image_types("RGB")
procedure.set_documentation (
_("Save as colored HTML text"),
"Saves the image as colored XHTML text (based on Perl version by Marc Lehmann)",
name)
procedure.set_menu_label(_("Colored HTML text"))
procedure.set_attribution("Manish Singh and Carol Spears",
"(c) GPL V3.0 or later",
"2003")
procedure.set_extensions ("html,xhtml");
procedure.add_argument_from_property(self, "source-file")
procedure.add_argument_from_property(self, "characters")
procedure.add_argument_from_property(self, "font-size")
procedure.add_argument_from_property(self, "separate")
return procedure
Pika.main(ColorXhtml.__gtype__, sys.argv)

View File

@ -0,0 +1,498 @@
#!/usr/bin/env python3
# PIKA 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 gi
gi.require_version('Pika', '3.0')
from gi.repository import Pika
gi.require_version('Gegl', '0.4')
from gi.repository import Gegl
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
import os, sys, tempfile, zipfile
import xml.etree.ElementTree as ET
NESTED_STACK_END = object()
layermodes_map = {
"svg:src-over": Pika.LayerMode.NORMAL,
"svg:multiply": Pika.LayerMode.MULTIPLY,
"svg:screen": Pika.LayerMode.SCREEN,
"svg:overlay": Pika.LayerMode.OVERLAY,
"svg:darken": Pika.LayerMode.DARKEN_ONLY,
"svg:lighten": Pika.LayerMode.LIGHTEN_ONLY,
"svg:color-dodge": Pika.LayerMode.DODGE,
"svg:color-burn": Pika.LayerMode.BURN,
"svg:hard-light": Pika.LayerMode.HARDLIGHT,
"svg:soft-light": Pika.LayerMode.SOFTLIGHT,
"svg:difference": Pika.LayerMode.DIFFERENCE,
"svg:color": Pika.LayerMode.HSL_COLOR,
"svg:luminosity": Pika.LayerMode.HSV_VALUE,
"svg:hue": Pika.LayerMode.HSV_HUE,
"svg:saturation": Pika.LayerMode.HSV_SATURATION,
"svg:plus": Pika.LayerMode.ADDITION,
}
# There are less svg blending ops than we have PIKA blend modes.
# We are going to map them as closely as possible.
pika_layermodes_map = {
Pika.LayerMode.NORMAL: "svg:src-over",
Pika.LayerMode.NORMAL_LEGACY: "svg:src-over",
Pika.LayerMode.MULTIPLY: "svg:multiply",
Pika.LayerMode.MULTIPLY_LEGACY: "svg:multiply",
Pika.LayerMode.SCREEN: "svg:screen",
Pika.LayerMode.SCREEN_LEGACY: "svg:screen",
Pika.LayerMode.OVERLAY: "svg:overlay",
Pika.LayerMode.OVERLAY_LEGACY: "svg:overlay",
Pika.LayerMode.DARKEN_ONLY: "svg:darken",
Pika.LayerMode.DARKEN_ONLY_LEGACY: "svg:darken",
Pika.LayerMode.LIGHTEN_ONLY: "svg:lighten",
Pika.LayerMode.LIGHTEN_ONLY_LEGACY: "svg:lighten",
Pika.LayerMode.DODGE: "svg:color-dodge",
Pika.LayerMode.DODGE_LEGACY: "svg:color-dodge",
Pika.LayerMode.BURN: "svg:color-burn",
Pika.LayerMode.BURN_LEGACY: "svg:color-burn",
Pika.LayerMode.HARDLIGHT: "svg:hard-light",
Pika.LayerMode.HARDLIGHT_LEGACY: "svg:hard-light",
Pika.LayerMode.SOFTLIGHT: "svg:soft-light",
Pika.LayerMode.SOFTLIGHT_LEGACY: "svg:soft-light",
Pika.LayerMode.DIFFERENCE: "svg:difference",
Pika.LayerMode.DIFFERENCE_LEGACY: "svg:difference",
Pika.LayerMode.HSL_COLOR: "svg:color",
Pika.LayerMode.HSL_COLOR_LEGACY: "svg:color",
Pika.LayerMode.HSV_VALUE: "svg:luminosity",
Pika.LayerMode.HSV_VALUE_LEGACY: "svg:luminosity",
Pika.LayerMode.HSV_HUE: "svg:hue",
Pika.LayerMode.HSV_HUE_LEGACY: "svg:hue",
Pika.LayerMode.HSV_SATURATION: "svg:saturation",
Pika.LayerMode.HSV_SATURATION_LEGACY: "svg:saturation",
Pika.LayerMode.ADDITION: "svg:plus",
Pika.LayerMode.ADDITION_LEGACY: "svg:plus",
# FIXME Determine the closest available layer mode
# Alternatively we could add additional modes
# e.g. something like "pika:dissolve", this
# is what Krita seems to do too
Pika.LayerMode.DISSOLVE: "svg:src-over",
Pika.LayerMode.DIVIDE: "svg:src-over",
Pika.LayerMode.DIVIDE_LEGACY: "svg:src-over",
Pika.LayerMode.BEHIND: "svg:src-over",
Pika.LayerMode.BEHIND_LEGACY: "svg:src-over",
Pika.LayerMode.GRAIN_EXTRACT: "svg:src-over",
Pika.LayerMode.GRAIN_EXTRACT_LEGACY: "svg:src-over",
Pika.LayerMode.GRAIN_MERGE: "svg:src-over",
Pika.LayerMode.GRAIN_MERGE_LEGACY: "svg:src-over",
Pika.LayerMode.COLOR_ERASE: "svg:src-over",
Pika.LayerMode.COLOR_ERASE_LEGACY: "svg:src-over",
Pika.LayerMode.LCH_HUE: "svg:src-over",
Pika.LayerMode.LCH_CHROMA: "svg:src-over",
Pika.LayerMode.LCH_COLOR: "svg:src-over",
Pika.LayerMode.LCH_LIGHTNESS: "svg:src-over",
Pika.LayerMode.SUBTRACT: "svg:src-over",
Pika.LayerMode.SUBTRACT_LEGACY: "svg:src-over",
Pika.LayerMode.VIVID_LIGHT: "svg:src-over",
Pika.LayerMode.PIN_LIGHT: "svg:src-over",
Pika.LayerMode.LINEAR_LIGHT: "svg:src-over",
Pika.LayerMode.HARD_MIX: "svg:src-over",
Pika.LayerMode.EXCLUSION: "svg:src-over",
Pika.LayerMode.LINEAR_BURN: "svg:src-over",
Pika.LayerMode.LUMA_DARKEN_ONLY: "svg:src-over",
Pika.LayerMode.LUMA_LIGHTEN_ONLY: "svg:src-over",
Pika.LayerMode.LUMINANCE: "svg:src-over",
Pika.LayerMode.COLOR_ERASE: "svg:src-over",
Pika.LayerMode.ERASE: "svg:src-over",
Pika.LayerMode.MERGE: "svg:src-over",
Pika.LayerMode.SPLIT: "svg:src-over",
Pika.LayerMode.PASS_THROUGH: "svg:src-over",
}
def reverse_map(mapping):
return dict((v,k) for k, v in mapping.items())
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, Pika.LayerMode.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, Pika.LayerMode.NORMAL)
return name, 0, 0, opac, visible, layer_mode
def thumbnail_ora(procedure, file, thumb_size, args, data):
tempdir = tempfile.mkdtemp('pika-plugin-file-openraster')
orafile = zipfile.ZipFile(file.peek_path())
stack, w, h = get_image_attributes(orafile)
# create temp file
tmp = os.path.join(tempdir, 'tmp.png')
with open(tmp, 'wb') as fid:
fid.write(orafile.read('Thumbnails/thumbnail.png'))
thumb_file = Gio.file_new_for_path(tmp)
result = Pika.get_pdb().run_procedure('file-png-load', [
GObject.Value(Pika.RunMode, Pika.RunMode.NONINTERACTIVE),
GObject.Value(Gio.File, thumb_file),
])
os.remove(tmp)
os.rmdir(tempdir)
if (result.index(0) == Pika.PDBStatusType.SUCCESS):
img = result.index(1)
# TODO: scaling
return Pika.ValueArray.new_from_values([
GObject.Value(Pika.PDBStatusType, Pika.PDBStatusType.SUCCESS),
GObject.Value(Pika.Image, img),
GObject.Value(GObject.TYPE_INT, w),
GObject.Value(GObject.TYPE_INT, h),
GObject.Value(Pika.ImageType, Pika.ImageType.RGB_IMAGE),
GObject.Value(GObject.TYPE_INT, 1)
])
else:
return procedure.new_return_values(result.index(0), GLib.Error(result.index(1)))
# We would expect the n_drawables parameter to not be there with introspection but
# currently that isn't working, see issue #5312. Until that is resolved we keep
# this parameter here or else saving would fail.
def save_ora(procedure, run_mode, image, n_drawables, drawables, file, args, data):
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)
Pika.progress_init("Exporting openraster image")
tempdir = tempfile.mkdtemp('pika-plugin-file-openraster')
# use .tmpsave extension, so we don't overwrite a valid file if
# there is an exception
orafile = zipfile.ZipFile(file.peek_path() + '.tmpsave', 'w', compression=zipfile.ZIP_STORED)
write_file_str(orafile, 'mimetype', 'image/openraster') # must be the first file written
# build image attributes
xml_image = ET.Element('image')
stack = ET.SubElement(xml_image, 'stack')
a = xml_image.attrib
a['w'] = str(image.get_width())
a['h'] = str(image.get_height())
def store_layer(image, drawable, path):
tmp = os.path.join(tempdir, 'tmp.png')
interlace, compression = 0, 2
Pika.get_pdb().run_procedure('file-png-save', [
GObject.Value(Pika.RunMode, Pika.RunMode.NONINTERACTIVE),
GObject.Value(Pika.Image, image),
GObject.Value(GObject.TYPE_INT, 1),
GObject.Value(Pika.ObjectArray, Pika.ObjectArray.new(Pika.Drawable, [drawable], False)),
GObject.Value(Gio.File, Gio.File.new_for_path(tmp)),
GObject.Value(GObject.TYPE_BOOLEAN, interlace),
GObject.Value(GObject.TYPE_INT, compression),
# write all PNG chunks except oFFs(ets)
GObject.Value(GObject.TYPE_BOOLEAN, True), # Save background color (bKGD chunk)
GObject.Value(GObject.TYPE_BOOLEAN, False), # Save layer offset (oFFs chunk)
GObject.Value(GObject.TYPE_BOOLEAN, True), # Save resolution (pHYs chunk)
GObject.Value(GObject.TYPE_BOOLEAN, True), # Save creation time (tIME chunk)
# Other settings
GObject.Value(GObject.TYPE_BOOLEAN, True), # Save color values from transparent pixels
])
if (os.path.exists(tmp)):
orafile.write(tmp, path)
os.remove(tmp)
else:
print("Error removing ", tmp)
def add_layer(parent, x, y, opac, pika_layer, path, visible=True):
store_layer(image, pika_layer, path)
# create layer attributes
layer = ET.Element('layer')
parent.append(layer)
a = layer.attrib
a['src'] = path
a['name'] = pika_layer.get_name()
a['x'] = str(x)
a['y'] = str(y)
a['opacity'] = str(opac)
a['visibility'] = 'visible' if visible else 'hidden'
a['composite-op'] = pika_layermodes_map.get(pika_layer.get_mode(), 'svg:src-over')
return layer
def add_group_layer(parent, opac, pika_layer, visible=True):
# create layer attributes
group_layer = ET.Element('stack')
parent.append(group_layer)
a = group_layer.attrib
a['name'] = pika_layer.get_name()
a['opacity'] = str(opac)
a['visibility'] = 'visible' if visible else 'hidden'
a['composite-op'] = pika_layermodes_map.get(pika_layer.get_mode(), 'svg:src-over')
return group_layer
def enumerate_layers(layers):
for layer in layers:
if not layer.is_group():
yield layer
else:
yield layer
for sublayer in enumerate_layers(layer.list_children()):
yield sublayer
yield NESTED_STACK_END
# save layers
parent_groups = []
i = 0
layer_stack = image.list_layers()
# Number of top level layers for tracking progress
lay_cnt = len(layer_stack)
for lay in enumerate_layers(layer_stack):
prev_lay = i
if lay is NESTED_STACK_END:
parent_groups.pop()
continue
_, x, y = lay.get_offsets()
opac = lay.get_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 lay.is_group():
group = add_group_layer(parent, opac, lay, lay.get_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.get_visible())
if (i > prev_lay):
Pika.progress_update(i/lay_cnt)
# save mergedimage
thumb = image.duplicate()
thumb_layer = thumb.merge_visible_layers (Pika.MergeType.CLIP_TO_IMAGE)
store_layer (thumb, thumb_layer, 'mergedimage.png')
# save thumbnail
w, h = image.get_width(), image.get_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, False)
if thumb.get_precision() != Pika.Precision.U8_GAMMA:
thumb.convert_precision (Pika.Precision.U8_GAMMA)
store_layer(thumb, thumb_layer, 'Thumbnails/thumbnail.png')
thumb.delete()
# write stack.xml
xml = ET.tostring(xml_image, encoding='UTF-8')
write_file_str(orafile, 'stack.xml', xml)
# finish up
orafile.close()
os.rmdir(tempdir)
if os.path.exists(file.peek_path()):
os.remove(file.peek_path()) # win32 needs that
os.rename(file.peek_path() + '.tmpsave', file.peek_path())
Pika.progress_end()
return Pika.ValueArray.new_from_values([
GObject.Value(Pika.PDBStatusType, Pika.PDBStatusType.SUCCESS)
])
def load_ora(procedure, run_mode, file, args, data):
tempdir = tempfile.mkdtemp('pika-plugin-file-openraster')
orafile = zipfile.ZipFile(file.peek_path())
stack, w, h = get_image_attributes(orafile)
Pika.progress_init("Loading openraster image")
img = Pika.Image.new(w, h, Pika.ImageBaseType.RGB)
img.set_file (file)
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 = []
# Number of top level layers for tracking progress
lay_cnt = len(stack)
layer_no = 0
for item in get_layers(stack):
prev_lay = layer_no
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)
pika_layer = Pika.Layer.group_new(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 pika cannot load files from inside a zip file
tmp = os.path.join(tempdir, 'tmp.png')
with open(tmp, 'wb') as fid:
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))
fid.write(data)
# import layer, set attributes and add to image
result = pika_layer = Pika.get_pdb().run_procedure('pika-file-load-layer', [
GObject.Value(Pika.RunMode, Pika.RunMode.NONINTERACTIVE),
GObject.Value(Pika.Image, img),
GObject.Value(Gio.File, Gio.File.new_for_path(tmp)),
])
if (result.index(0) == Pika.PDBStatusType.SUCCESS):
pika_layer = pika_layer.index(1)
os.remove(tmp)
else:
print("Error loading layer from openraster image.")
pika_layer.set_name(name)
pika_layer.set_mode(layer_mode)
pika_layer.set_offsets(x, y) # move to correct position
pika_layer.set_opacity(opac * 100) # a float between 0 and 100
pika_layer.set_visible(visible)
img.insert_layer(pika_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 pika_layer.is_group():
parent_groups.append([pika_layer, 0])
if (layer_no > prev_lay):
Pika.progress_update(layer_no/lay_cnt)
Pika.progress_end()
os.rmdir(tempdir)
return Pika.ValueArray.new_from_values([
GObject.Value(Pika.PDBStatusType, Pika.PDBStatusType.SUCCESS),
GObject.Value(Pika.Image, img),
])
class FileOpenRaster (Pika.PlugIn):
## PikaPlugIn virtual methods ##
def do_set_i18n(self, procname):
return True, 'pika30-python', None
def do_query_procedures(self):
return [ 'file-openraster-load-thumb',
'file-openraster-load',
'file-openraster-save' ]
def do_create_procedure(self, name):
if name == 'file-openraster-save':
procedure = Pika.SaveProcedure.new(self, name,
Pika.PDBProcType.PLUGIN,
save_ora, None)
procedure.set_image_types("*");
procedure.set_documentation ('save an OpenRaster (.ora) file',
'save an OpenRaster (.ora) file',
name)
procedure.set_menu_label('OpenRaster')
procedure.set_extensions ("ora");
elif name == 'file-openraster-load':
procedure = Pika.LoadProcedure.new (self, name,
Pika.PDBProcType.PLUGIN,
load_ora, None)
procedure.set_menu_label('OpenRaster')
procedure.set_documentation ('load an OpenRaster (.ora) file',
'load an OpenRaster (.ora) file',
name)
procedure.set_mime_types ("image/openraster");
procedure.set_extensions ("ora");
procedure.set_thumbnail_loader ('file-openraster-load-thumb');
else: # 'file-openraster-load-thumb'
procedure = Pika.ThumbnailProcedure.new (self, name,
Pika.PDBProcType.PLUGIN,
thumbnail_ora, None)
procedure.set_documentation ('loads a thumbnail from an OpenRaster (.ora) file',
'loads a thumbnail from an OpenRaster (.ora) file',
name)
procedure.set_attribution('Jon Nordby', #author
'Jon Nordby', #copyright
'2009') #year
return procedure
Pika.main(FileOpenRaster.__gtype__, sys.argv)

156
plug-ins/python/foggify.py Normal file
View File

@ -0,0 +1,156 @@
#!/usr/bin/env python3
# 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 gi
gi.require_version('Pika', '3.0')
from gi.repository import Pika
gi.require_version('PikaUi', '3.0')
from gi.repository import PikaUi
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
import time
import sys
def N_(message): return message
def _(message): return GLib.dgettext(None, message)
def foggify(procedure, run_mode, image, n_drawables, drawables, args, data):
config = procedure.create_config()
config.begin_run(image, run_mode, args)
if run_mode == Pika.RunMode.INTERACTIVE:
PikaUi.init('python-fu-foggify')
dialog = PikaUi.ProcedureDialog(procedure=procedure, config=config)
dialog.fill(None)
if not dialog.run():
dialog.destroy()
config.end_run(Pika.PDBStatusType.CANCEL)
return procedure.new_return_values(Pika.PDBStatusType.CANCEL, GLib.Error())
else:
dialog.destroy()
color = config.get_property('color')
name = config.get_property('name')
turbulence = config.get_property('turbulence')
opacity = config.get_property('opacity')
Pika.context_push()
image.undo_group_start()
if image.get_base_type() is Pika.ImageBaseType.RGB:
type = Pika.ImageType.RGBA_IMAGE
else:
type = Pika.ImageType.GRAYA_IMAGE
for drawable in drawables:
fog = Pika.Layer.new(image, name,
drawable.get_width(), drawable.get_height(),
type, opacity,
Pika.LayerMode.NORMAL)
fog.fill(Pika.FillType.TRANSPARENT)
image.insert_layer(fog, drawable.get_parent(),
image.get_item_position(drawable))
Pika.context_set_background(color)
fog.edit_fill(Pika.FillType.BACKGROUND)
# create a layer mask for the new layer
mask = fog.create_mask(0)
fog.add_mask(mask)
# add some clouds to the layer
Pika.get_pdb().run_procedure('plug-in-plasma', [
GObject.Value(Pika.RunMode, Pika.RunMode.NONINTERACTIVE),
GObject.Value(Pika.Image, image),
GObject.Value(Pika.Drawable, mask),
GObject.Value(GObject.TYPE_INT, int(time.time())),
GObject.Value(GObject.TYPE_DOUBLE, turbulence),
])
# apply the clouds to the layer
fog.remove_mask(Pika.MaskApplyMode.APPLY)
fog.set_visible(True)
Pika.displays_flush()
image.undo_group_end()
Pika.context_pop()
config.end_run(Pika.PDBStatusType.SUCCESS)
return procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
_color = Pika.RGB()
_color.set(240.0, 0, 0)
class Foggify (Pika.PlugIn):
## Parameters ##
__gproperties__ = {
"name": (str,
_("Layer _name"),
_("Layer name"),
_("Clouds"),
GObject.ParamFlags.READWRITE),
"turbulence": (float,
_("_Turbulence"),
_("Turbulence"),
0.0, 7.0, 1.0,
GObject.ParamFlags.READWRITE),
"opacity": (float,
_("O_pacity"),
_("Opacity"),
0.0, 100.0, 100.0,
GObject.ParamFlags.READWRITE),
}
# I use a different syntax for this property because I think it is
# supposed to allow setting a default, except it doesn't seem to
# work. I still leave it this way for now until we figure this out
# as it should be the better syntax.
color = GObject.Property(type =Pika.RGB, default=_color,
nick =_("_Fog color"),
blurb=_("Fog color"))
## PikaPlugIn virtual methods ##
def do_set_i18n(self, procname):
return True, 'pika30-python', None
def do_query_procedures(self):
return [ 'python-fu-foggify' ]
def do_create_procedure(self, name):
procedure = Pika.ImageProcedure.new(self, name,
Pika.PDBProcType.PLUGIN,
foggify, None)
procedure.set_image_types("RGB*, GRAY*");
procedure.set_sensitivity_mask (Pika.ProcedureSensitivityMask.DRAWABLE |
Pika.ProcedureSensitivityMask.DRAWABLES)
procedure.set_documentation (_("Add a layer of fog"),
_("Adds a layer of fog to the image."),
name)
procedure.set_menu_label(_("_Fog..."))
procedure.set_attribution("James Henstridge",
"James Henstridge",
"1999,2007")
procedure.add_menu_path ("<Image>/Filters/Decor")
procedure.add_argument_from_property(self, "name")
procedure.add_argument_from_property(self, "color")
procedure.add_argument_from_property(self, "turbulence")
procedure.add_argument_from_property(self, "opacity")
return procedure
Pika.main(Foggify.__gtype__, sys.argv)

View File

@ -0,0 +1,239 @@
#!/usr/bin/env python3
# -*- 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
import gi
gi.require_version('Pika', '3.0')
from gi.repository import Pika
gi.require_version('PikaUi', '3.0')
from gi.repository import PikaUi
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import time
import sys
def N_(message): return message
def _(message): return GLib.dgettext(None, message)
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)" % (c.r, c.g, c.b)
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(procedure, args, data):
if args.length() != 3:
error = 'Wrong parameters given'
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
GLib.Error(error))
runmode = args.index(0)
gradient = args.index(1)
file = args.index(2)
config = procedure.create_config()
config.begin_run(None, Pika.RunMode.INTERACTIVE, args)
if runmode == Pika.RunMode.INTERACTIVE:
PikaUi.init('python-fu-gradient-save-as-css')
dialog = PikaUi.ProcedureDialog(procedure=procedure, config=config)
# Add gradient button
dialog.fill (["gradient"])
# UI for the file parameter
# from histogram-export.py
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
homogeneous=False, spacing=10)
dialog.get_content_area().add(vbox)
vbox.show()
grid = Gtk.Grid()
grid.set_column_homogeneous(False)
grid.set_border_width(10)
grid.set_column_spacing(10)
grid.set_row_spacing(10)
vbox.add(grid)
grid.show()
def choose_file(widget):
if file_chooser_dialog.run() == Gtk.ResponseType.OK:
if file_chooser_dialog.get_file() is not None:
config.set_property("file", file_chooser_dialog.get_file())
file_entry.set_text(file_chooser_dialog.get_file().get_path())
file_chooser_dialog.hide()
file_chooser_button = Gtk.Button.new_with_mnemonic(label=_("_File..."))
grid.attach(file_chooser_button, 0, 0, 1, 1)
file_chooser_button.show()
file_chooser_button.connect("clicked", choose_file)
file_entry = Gtk.Entry.new()
grid.attach(file_entry, 1, 0, 1, 1)
file_entry.set_width_chars(40)
file_entry.set_placeholder_text(_("Choose CSS file..."))
if config.get_property ("file") != None:
file = config.get_property("file")
if file is not None:
file_entry.set_text(file.get_path())
file_entry.show()
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
file_chooser_dialog = Gtk.FileChooserDialog(use_header_bar=use_header_bar,
title=_("Save as CSS file..."),
action=Gtk.FileChooserAction.SAVE)
file_chooser_dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
file_chooser_dialog.add_button(_("_OK"), Gtk.ResponseType.OK)
#Connect config so reset works on custom widget
def gradient_reset (*args):
if len(args) >= 2:
if config.get_property("file") is not None and config.get_property("file").get_path() != file_entry.get_text():
file_entry.set_text(config.get_property("file").get_path())
else:
file_entry.set_text("")
config.connect("notify::gradient", gradient_reset)
config.connect("notify::file", gradient_reset)
if not dialog.run():
dialog.destroy()
config.end_run(Pika.PDBStatusType.CANCEL)
return procedure.new_return_values(Pika.PDBStatusType.CANCEL, GLib.Error())
else:
gradient = config.get_property("gradient")
file = Gio.file_new_for_path(file_entry.get_text())
#Save configs for non-connected UI element
config.set_property ("file", file)
dialog.destroy()
if file is None:
error = 'No file given'
config.end_run(Pika.PDBStatusType.CALLING_ERROR)
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
GLib.Error(error))
stops = []
wk_stops = []
n_segments = gradient.get_number_of_segments()
last_stop = None
for index in range(n_segments):
success, lcolor, lopacity = gradient.segment_get_left_color(index)
success, rcolor, ropacity = gradient.segment_get_right_color(index)
success, lpos = gradient.segment_get_left_pos(index)
success, rpos = gradient.segment_get_right_pos(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)
success, etag = file.replace_contents(bytes(format_text(final_text), encoding='utf-8'),
etag=None,
make_backup=False,
flags=Gio.FileCreateFlags.REPLACE_DESTINATION,
cancellable=None)
if success:
config.end_run(Pika.PDBStatusType.SUCCESS)
return procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
else:
config.end_run(Pika.PDBStatusType.EXECUTION_ERROR)
return procedure.new_return_values(Pika.PDBStatusType.EXECUTION_ERROR,
GLib.Error('File saving failed: {}'.format(file.get_path())))
class GradientsSaveAsCSS (Pika.PlugIn):
## Parameters ##
__gproperties__ = {
"run-mode": (Pika.RunMode,
_("Run mode"),
_("The run mode"),
Pika.RunMode.NONINTERACTIVE,
GObject.ParamFlags.READWRITE),
"gradient": (Pika.Gradient,
_("_Gradient to use"), "",
GObject.ParamFlags.READWRITE),
"file": (Gio.File,
_("_File"), None,
GObject.ParamFlags.READWRITE),
}
## PikaPlugIn virtual methods ##
def do_set_i18n(self, procname):
return True, 'pika30-python', None
def do_query_procedures(self):
return [ 'gradient-save-as-css' ]
def do_create_procedure(self, name):
procedure = Pika.Procedure.new(self, name,
Pika.PDBProcType.PLUGIN,
gradient_css_save, None)
if name == 'gradient-save-as-css':
procedure.set_documentation (_("Creates a new palette from a given gradient"),
_("Creates a new palette from a given gradient"),
name)
procedure.set_menu_label(_("Save as CSS..."))
procedure.set_attribution("Joao S. O. Bueno",
"(c) GPL V3.0 or later",
"2011")
procedure.add_menu_path('<Gradients>')
procedure.add_argument_from_property(self, "run-mode")
procedure.add_argument_from_property(self, "gradient")
procedure.add_argument_from_property(self, "file")
return procedure
Pika.main(GradientsSaveAsCSS.__gtype__, sys.argv)

View File

@ -0,0 +1,369 @@
#!/usr/bin/env python3
#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 columns 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 pika-histogram call
"""
import csv
import math
import sys
import gi
gi.require_version('Pika', '3.0')
from gi.repository import Pika
gi.require_version('PikaUi', '3.0')
from gi.repository import PikaUi
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
def N_(message): return message
def _(message): return GLib.dgettext(None, message)
class StringEnum:
"""
Helper class for when you want to use strings as keys of an enum. The values would be
user facing strings that might undergo translation.
The constructor accepts an even amount of arguments. Each pair of arguments
is a key/value pair.
"""
def __init__(self, *args):
self.keys = []
self.values = []
for i in range(len(args)//2):
self.keys.append(args[i*2])
self.values.append(args[i*2+1])
def get_tree_model(self):
""" Get a tree model that can be used in GTK widgets. """
tree_model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING)
for i in range(len(self.keys)):
tree_model.append([self.keys[i], self.values[i]])
return tree_model
def __getattr__(self, name):
""" Implements access to the key. For example, if you provided a key "red", then you could access it by
referring to
my_enum.red
It may seem silly as "my_enum.red" is longer to write then just "red",
but this provides verification that the key is indeed inside enum. """
key = name.replace("_", " ")
if key in self.keys:
return key
raise AttributeError("No such key string " + key)
output_format_enum = StringEnum(
"pixel count", _("Pixel count"),
"normalized", _("Normalized"),
"percent", _("Percent")
)
def histogram_export(procedure, img, layers, gio_file,
bucket_size, sample_average, output_format,
progress_bar):
layers = img.get_selected_layers()
layer = layers[0]
if sample_average:
new_img = img.duplicate()
layer = new_img.merge_visible_layers(Pika.MergeType.CLIP_TO_IMAGE)
channels_txt = ["Value"]
channels_pika = [Pika.HistogramChannel.VALUE]
if layer.is_rgb():
channels_txt += ["Red", "Green", "Blue", "Luminance"]
channels_pika += [Pika.HistogramChannel.RED, Pika.HistogramChannel.GREEN, Pika.HistogramChannel.BLUE,
Pika.HistogramChannel.LUMINANCE]
if layer.has_alpha():
channels_txt += ["Alpha"]
channels_pika += [Pika.HistogramChannel.ALPHA]
try:
with open(gio_file.get_path(), "wt") as hfile:
writer = csv.writer(hfile)
# Write headers:
writer.writerow(["Range start"] + channels_txt)
max_index = 1.0/bucket_size if bucket_size > 0 else 1
i = 0
progress_bar_int_percent = 0
while True:
start_range = i * bucket_size
i += 1
if start_range >= 1.0:
break
row = [start_range]
for channel in channels_pika:
result = Pika.get_pdb().run_procedure('pika-drawable-histogram',
[ GObject.Value(Pika.Drawable, layer),
GObject.Value(Pika.HistogramChannel, channel),
GObject.Value(GObject.TYPE_DOUBLE,
float(start_range)),
GObject.Value(GObject.TYPE_DOUBLE,
float(min(start_range + bucket_size, 1.0))) ])
if output_format == output_format_enum.pixel_count:
count = int(result.index(5))
else:
pixels = result.index(4)
count = (result.index(5) / pixels) if pixels else 0
if output_format == output_format_enum.percent:
count = "%.2f%%" % (count * 100)
row.append(str(count))
writer.writerow(row)
# Update progress bar
if progress_bar:
fraction = i / max_index
# Only update the progress bar if it changed at least 1% .
new_percent = math.floor(fraction * 100)
if new_percent != progress_bar_int_percent:
progress_bar_int_percent = new_percent
progress_bar.set_fraction(fraction)
# Make sure the progress bar gets drawn on screen.
while Gtk.events_pending():
Gtk.main_iteration()
except IsADirectoryError:
return procedure.new_return_values(Pika.PDBStatusType.EXECUTION_ERROR,
GLib.Error(_("File is either a directory or file name is empty.")))
except FileNotFoundError:
return procedure.new_return_values(Pika.PDBStatusType.EXECUTION_ERROR,
GLib.Error(_("Directory not found.")))
except PermissionError:
return procedure.new_return_values(Pika.PDBStatusType.EXECUTION_ERROR,
GLib.Error("You do not have permissions to write that file."))
if sample_average:
new_img.delete()
return procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
def run(procedure, run_mode, image, n_layers, layers, args, data):
gio_file = args.index(0)
bucket_size = args.index(1)
sample_average = args.index(2)
output_format = args.index(3)
progress_bar = None
config = None
if run_mode == Pika.RunMode.INTERACTIVE:
config = procedure.create_config()
# Set properties from arguments. These properties will be changed by the UI.
#config.set_property("file", gio_file)
#config.set_property("bucket_size", bucket_size)
#config.set_property("sample_average", sample_average)
#config.set_property("output_format", output_format)
config.begin_run(image, run_mode, args)
PikaUi.init("histogram-export.py")
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
dialog = PikaUi.Dialog(use_header_bar=use_header_bar,
title=_("Histogram Export..."))
dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
dialog.add_button(_("_OK"), Gtk.ResponseType.OK)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
homogeneous=False, spacing=10)
dialog.get_content_area().add(vbox)
vbox.show()
# Create grid to set all the properties inside.
grid = Gtk.Grid()
grid.set_column_homogeneous(False)
grid.set_border_width(10)
grid.set_column_spacing(10)
grid.set_row_spacing(10)
vbox.add(grid)
grid.show()
# UI for the file parameter
def choose_file(widget):
if file_chooser_dialog.run() == Gtk.ResponseType.OK:
if file_chooser_dialog.get_file() is not None:
config.set_property("file", file_chooser_dialog.get_file())
file_entry.set_text(file_chooser_dialog.get_file().get_path())
file_chooser_dialog.hide()
file_chooser_button = Gtk.Button.new_with_mnemonic(label=_("_File..."))
grid.attach(file_chooser_button, 0, 0, 1, 1)
file_chooser_button.show()
file_chooser_button.connect("clicked", choose_file)
file_entry = Gtk.Entry.new()
grid.attach(file_entry, 1, 0, 1, 1)
file_entry.set_width_chars(40)
file_entry.set_placeholder_text(_("Choose export file..."))
if gio_file is not None:
file_entry.set_text(gio_file.get_path())
file_entry.show()
file_chooser_dialog = Gtk.FileChooserDialog(use_header_bar=use_header_bar,
title=_("Histogram Export file..."),
action=Gtk.FileChooserAction.SAVE)
file_chooser_dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
file_chooser_dialog.add_button(_("_OK"), Gtk.ResponseType.OK)
# Bucket size parameter
label = Gtk.Label.new_with_mnemonic(_("_Bucket Size"))
grid.attach(label, 0, 1, 1, 1)
label.show()
spin = PikaUi.prop_spin_button_new(config, "bucket_size", step_increment=0.001, page_increment=0.1, digits=3)
grid.attach(spin, 1, 1, 1, 1)
spin.show()
# Sample average parameter
spin = PikaUi.prop_check_button_new(config, "sample_average", _("Sample _Average"))
spin.set_tooltip_text(_("If checked, the histogram is generated from merging all visible layers."
" Otherwise, the histogram is only for the current layer."))
grid.attach(spin, 1, 2, 1, 1)
spin.show()
# Output format parameter
label = Gtk.Label.new_with_mnemonic(_("_Output Format"))
grid.attach(label, 0, 3, 1, 1)
label.show()
combo = PikaUi.prop_string_combo_box_new(config, "output_format", output_format_enum.get_tree_model(), 0, 1)
grid.attach(combo, 1, 3, 1, 1)
combo.show()
progress_bar = Gtk.ProgressBar()
vbox.add(progress_bar)
progress_bar.show()
dialog.show()
if dialog.run() != Gtk.ResponseType.OK:
return procedure.new_return_values(Pika.PDBStatusType.CANCEL,
GLib.Error())
# Extract values from UI
gio_file = Gio.file_new_for_path(file_entry.get_text()) # config.get_property("file")
bucket_size = config.get_property("bucket_size")
sample_average = config.get_property("sample_average")
output_format = config.get_property("output_format")
if gio_file is None:
error = 'No file given'
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
GLib.Error(error))
result = histogram_export(procedure, image, layers, gio_file,
bucket_size, sample_average, output_format, progress_bar)
# If the execution was successful, save parameters so they will be restored next time we show dialog.
if result.index(0) == Pika.PDBStatusType.SUCCESS and config is not None:
config.end_run(Pika.PDBStatusType.SUCCESS)
return result
class HistogramExport(Pika.PlugIn):
## Parameters ##
__gproperties__ = {
# "filename": (str,
# # TODO: I wanted this property to be a path (and not just str) , so I could use
# # prop_file_chooser_button_new to open a file dialog. However, it fails without an error message.
# # Pika.ConfigPath,
# _("Histogram _File"),
# _("Histogram _File"),
# "histogram_export.csv",
# # Pika.ConfigPathType.FILE,
# GObject.ParamFlags.READWRITE),
"file": (Gio.File,
_("Histogram _File"),
"Histogram export file",
GObject.ParamFlags.READWRITE),
"bucket_size": (float,
_("_Bucket Size"),
"Bucket Size",
0.001, 1.0, 0.01,
GObject.ParamFlags.READWRITE),
"sample_average": (bool,
_("Sample _Average"),
"Sample Average",
False,
GObject.ParamFlags.READWRITE),
"output_format": (str,
_("Output format"),
"Output format: 'pixel count', 'normalized', 'percent'",
"pixel count",
GObject.ParamFlags.READWRITE),
}
## PikaPlugIn virtual methods ##
def do_set_i18n(self, procname):
return True, 'pika30-python', None
def do_query_procedures(self):
return ['histogram-export']
def do_create_procedure(self, name):
procedure = None
if name == 'histogram-export':
procedure = Pika.ImageProcedure.new(self, name,
Pika.PDBProcType.PLUGIN,
run, None)
procedure.set_image_types("*")
procedure.set_documentation (
_("Exports the image histogram to a text file (CSV)"),
globals()["__doc__"], # This includes the docstring, on the top of the file
name)
procedure.set_menu_label(_("_Export histogram..."))
procedure.set_attribution("João S. O. Bueno",
"(c) GPL V3.0 or later",
"2014")
procedure.add_menu_path("<Image>/Colors/Info/")
procedure.add_argument_from_property(self, "file")
procedure.add_argument_from_property(self, "bucket_size")
procedure.add_argument_from_property(self, "sample_average")
procedure.add_argument_from_property(self, "output_format")
return procedure
Pika.main(HistogramExport.__gtype__, sys.argv)

View File

@ -0,0 +1,53 @@
if not have_python
subdir_done()
endif
plugins = [
{ 'name': 'colorxhtml' },
{ 'name': 'file-openraster' },
{ 'name': 'foggify' },
{ 'name': 'gradients-save-as-css' },
{ 'name': 'histogram-export' },
{ 'name': 'palette-offset' },
{ 'name': 'palette-sort' },
{ 'name': 'palette-to-gradient' },
{ 'name': 'python-eval' },
{ 'name': 'spyro-plus' },
]
if not stable or not release
plugins += [
{ 'name': 'test-dialog' },
]
endif
subdir('python-console')
foreach plugin : plugins
name = plugin.get('name')
srcs = plugin.get('srcs', [name + '.py'])
install_data(srcs, install_dir: pikaplugindir / 'plug-ins' / name,
install_mode: 'rwxr-xr-x')
foreach src : srcs
# Ugly trick to copy Python plug-ins into subfolders so that we can run PIKA
# from the build directory without installing it.
run_command(python, meson.project_source_root() / '.gitlab/cp-plug-in-subfolder.py',
meson.current_source_dir() / src, meson.current_build_dir() / name, check: true)
endforeach
endforeach
if python.found()
python_config = configuration_data()
python_config.set('PYTHON_PATH', python.full_path())
configure_file(
input : 'pypika.interp.in',
output: 'pypika.interp',
configuration: python_config,
install: true,
install_dir: pikaplugindir / 'interpreters',
)
endif

View File

@ -0,0 +1,195 @@
#!/usr/bin/env python3
# -*- 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/>.
import gi
gi.require_version('Pika', '3.0')
from gi.repository import Pika
gi.require_version('PikaUi', '3.0')
from gi.repository import PikaUi
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
import sys
def N_(message): return message
def _(message): return GLib.dgettext(None, message)
help_doc = r"""
Offset the colors in the palette.
Offsets and returns the given palette when it is editable,
otherwise copies the given palette and returns it.
"""
class PaletteOffset (Pika.PlugIn):
## Parameter: run-mode ##
@GObject.Property(type=Pika.RunMode,
default=Pika.RunMode.NONINTERACTIVE,
nick="Run mode", blurb="The run mode")
def run_mode(self):
"""Read-write integer property."""
return self._run_mode
@run_mode.setter
def run_mode(self, run_mode):
self._run_mode = run_mode
## Parameter: palette ##
@GObject.Property(type=Pika.Palette,
nick= _("Palette"),
blurb= _("Palette"))
def palette(self):
return self._palette
@palette.setter
def palette(self, palette):
self._palette = palette
## Parameter: amount ##
@GObject.Property(type=int,
default=1,
nick= _("Off_set"),
blurb= _("Offset"))
def amount(self):
return self._amount
@amount.setter
def amount(self, amount):
self._amount = amount
## Return: new-palette ##
@GObject.Property(type=Pika.Palette,
nick=_("The edited palette"),
blurb=_("The newly created palette when read-only, otherwise the input palette"))
def new_palette(self):
return self.new_palette
@new_palette.setter
def new_palette(self, new_palette):
self.new_palette = new_palette
## PikaPlugIn virtual methods ##
def do_set_i18n(self, procname):
return True, 'pika30-python', None
def do_query_procedures(self):
return [ "python-fu-palette-offset" ]
def do_create_procedure(self, name):
procedure = Pika.Procedure.new(self, name,
Pika.PDBProcType.PLUGIN,
self.run, None)
if name == 'python-fu-palette-offset':
procedure.set_menu_label(_("_Offset Palette..."))
procedure.set_documentation(_("Offset the colors in a palette"),
help_doc,
"")
procedure.set_attribution("Joao S. O. Bueno Calligaris, Carol Spears",
"(c) Joao S. O. Bueno Calligaris",
"2004, 2006")
procedure.add_argument_from_property(self, "run-mode")
procedure.add_argument_from_property(self, "palette")
procedure.add_argument_from_property(self, "amount")
procedure.add_return_value_from_property(self, "new-palette")
procedure.add_menu_path ('<Palettes>')
else:
procedure = None
return procedure
def run(self, procedure, args, data):
palette = None
amount = 1
# Get the parameters
if args.length() < 1:
error = 'No parameters given'
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
GLib.Error(error))
runmode = args.index(0)
if args.length() > 1:
palette = args.index(1)
if palette is None:
palette = Pika.context_get_palette()
if not palette.is_valid():
error = f'Invalid palette ID: {palette.get_id()}'
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
GLib.Error(error))
if args.length() > 2:
amount = args.index(2)
if runmode == Pika.RunMode.INTERACTIVE:
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
PikaUi.init ("palette-offset.py")
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
dialog = PikaUi.Dialog(use_header_bar=use_header_bar,
title=_("Offset Palette..."))
dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
dialog.add_button(_("_OK"), Gtk.ResponseType.OK)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
homogeneous=False, spacing=12)
dialog.get_content_area().add(box)
box.show()
label = Gtk.Label.new(_("Offset"))
box.pack_start(label, False, False, 1)
label.show()
amount = self.set_property("amount", amount)
spin = PikaUi.prop_spin_button_new(self, "amount", 1.0, 5.0, 0)
spin.set_activates_default(True)
box.pack_end(spin, False, False, 1)
spin.show()
dialog.show()
if dialog.run() != Gtk.ResponseType.OK:
print ("Canceled")
return procedure.new_return_values(Pika.PDBStatusType.CANCEL,
GLib.Error("Canceled"))
amount = self.get_property("amount")
#If palette is read only, work on a copy:
editable = palette.is_editable()
if not editable:
palette = palette.duplicate()
num_colors = palette.get_color_count()
tmp_entry_array = []
for i in range (num_colors):
tmp_entry_array.append ((palette.entry_get_name(i)[1],
palette.entry_get_color(i)[1]))
for i in range (num_colors):
target_index = i + amount
if target_index >= num_colors:
target_index -= num_colors
elif target_index < 0:
target_index += num_colors
palette.entry_set_name(target_index, tmp_entry_array[i][0])
palette.entry_set_color(target_index, tmp_entry_array[i][1])
retval = procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
value = GObject.Value(Pika.Palette, palette)
retval.remove(1)
retval.insert(1, value)
return retval
Pika.main(PaletteOffset.__gtype__, sys.argv)

View File

@ -0,0 +1,524 @@
#!/usr/bin/env python3
# -*- 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/>.
# For a detailed explanation of the parameters of this plugin, see :
# https://gitlab.gnome.org/GNOME/pika/-/issues/4368#note_763460
# little known, colorsys is part of Python's stdlib
from colorsys import rgb_to_yiq
from textwrap import dedent
from random import randint
import gi
gi.require_version('Pika', '3.0')
from gi.repository import Pika
gi.require_version('PikaUi', '3.0')
from gi.repository import PikaUi
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import sys
def N_(message): return message
def _(message): return GLib.dgettext(None, message)
AVAILABLE_CHANNELS = (_("Red"), _("Green"), _("Blue"),
_("Luma (Y)"),
_("Hue"), _("Saturation"), _("Value"),
_("Saturation (HSL)"), _("Lightness (HSL)"),
_("Index"),
_("Random"))
channel_getters = [
(lambda v, i: v.r),
(lambda v, i: v.g),
(lambda v, i: v.r),
(lambda v, i: rgb_to_yiq(v.r, v.g, v.b)[0]),
(lambda v, i: v.to_hsv().h),
(lambda v, i: v.to_hsv().s),
(lambda v, i: v.to_hsv().v),
(lambda v, i: v.to_hsl().s),
(lambda v, i: v.to_hsl().l),
(lambda v, i: i),
(lambda v, i: randint(0, 0x7fffffff))
]
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)
try:
from colormath.color_objects import RGBColor, LabColor, LCHabColor
def to_lab(v):
return RGBColor(v.r, v.g, v.b).convert_to('LAB').get_value_tuple()
def to_lchab(v):
return RGBColor(v.r, v.g, v.b).convert_to('LCHab').get_value_tuple()
AVAILABLE_CHANNELS = AVAILABLE_CHANNELS + (_("Lightness (LAB)"),
_("A-color"), _("B-color"),
_("Chroma (LCHab)"),
_("Hue (LCHab)"))
channel_getters.extend([
(lambda v, i: to_lab(v)[0]),
(lambda v, i: to_lab(v)[1]),
(lambda v, i: to_lab(v)[2]),
(lambda v, i: to_lchab(v)[1]),
(lambda v, i: to_lchab(v)[2])
])
except ImportError:
pass
slice_expr_doc = """
Format is 'start:nrows,length' . All items are optional.
The empty string 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)
':3,4' selects 3 rows of 4 colors
'4:' selects a single row of all colors after 4, inclusive.
'3:,4' selects rows of 4 colors, starting at 3 (nrows auto-determined)
'2:3,4' selects 3 rows of 4 colors (12 colors total), beginning at index 2.
'4' is illegal (ambiguous)
"""
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.
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 = palette.is_editable()
if not editable:
palette = palette.duplicate()
num_colors = palette.get_color_count()
start, nrows, length = None, None, None
if selection == SELECT_AUTOSLICE:
def find_index(color, startindex=0):
for i in range(startindex, num_colors):
c = palette.entry_get_color(i)
if c[1].r == color[1].r and c[1].g == color[1].g and c[1].b == color[1].b:
return i
return None
def hexcolor(c):
return "#%02x%02x%02x" % (int(255 * c[1].r), int(255 * c[1].b), int(255 * c[1].g))
fg = Pika.context_get_foreground()
bg = Pika.context_get_background()
start = find_index(fg)
end = find_index(bg)
if start is None:
raise ValueError("Couldn't find foreground color %s in palette" % hexcolor(fg))
if end is None:
raise ValueError("Couldn't find background color %s in palette" % hexcolor(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 behavior 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_getters[channel1]
channels_getter_2 = channel_getters[channel2]
def get_colors(start, end):
result = []
for i in range(start, end):
entry = (palette.entry_get_name(i)[1],
palette.entry_get_color(i)[1])
index1 = channels_getter_1(entry[1], i)
index2 = channels_getter_2(entry[1], i)
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):
palette.entry_set_name(i, entry_list[i][1][0])
palette.entry_set_color(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 = channel_getters[pchannel]
for row in range(nrows):
partition_spans = [1]
rowstart = start + (row * length)
old_color = palette.entry_get_color(rowstart)[1]
old_partition = pchannels_getter(old_color, rowstart)
old_partition = old_partition - (old_partition % pgrain)
for i in range(rowstart + 1, rowstart + length):
this_color = palette.entry_get_color(i)[1]
this_partition = pchannels_getter(this_color, i)
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(SELECT_SLICE, '%d:1,%d' % (base, size),
channel1, ascending1,
channel2, ascending2,
quantize, 0, 1.0)
base += size
else:
# SELECT_SLICE and SELECT_AUTOSLICE
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):
palette.entry_set_name(i, entry[1][0])
palette.entry_set_color(i, entry[1][1])
return palette
# FIXME: Write humanly readable help -
# See for reference: https://gitlab.gnome.org/GNOME/pika/-/issues/4368#note_763460
# Important to describe the general effect on palettes rather than details of the sort.
help_doc = r"""
Sorts a palette, or part of a palette.
Sorts the given palette when it is editable, otherwise creates a new sorted palette.
The default is a 1D sort, but you can also sort over two color channels
or create a 2D sorted palette with sorted rows.
You can optionally install colormath (https://pypi.python.org/pypi/colormath/1.0.8)
to PIKA's Python to get even more channels to choose from.
"""
selections_option = [ _("All"), _("Slice / Array"), _("Autoslice (fg->bg)"), _("Partitioned") ]
class PaletteSort (Pika.PlugIn):
## Parameters ##
__gproperties__ = {
"run-mode": (Pika.RunMode,
_("Run mode"),
"The run mode",
Pika.RunMode.INTERACTIVE,
GObject.ParamFlags.READWRITE),
"palette": (Pika.Palette,
_("_Palette"),
_("Palette"),
GObject.ParamFlags.READWRITE),
"selections": (int,
_("Select_ions"),
str(selections_option),
0, 3, 0,
GObject.ParamFlags.READWRITE),
# TODO: It would be much simpler to replace the slice expression with three
# separate parameters: start-index, number-of-rows, row_length
"slice_expr": (str,
_("Slice _expression"),
slice_expr_doc,
"",
GObject.ParamFlags.READWRITE),
"channel1": (int,
_("Channel _to sort"),
"Channel to sort: " + str(AVAILABLE_CHANNELS),
0, len(AVAILABLE_CHANNELS), 3,
GObject.ParamFlags.READWRITE),
"ascending1": (bool,
_("_Ascending"),
_("Ascending"),
True,
GObject.ParamFlags.READWRITE),
"channel2": (int,
_("Secondary C_hannel to sort"),
"Secondary Channel to sort: " + str(AVAILABLE_CHANNELS),
0, len(AVAILABLE_CHANNELS), 5,
GObject.ParamFlags.READWRITE),
"ascending2": (bool,
_("Ascen_ding"),
_("Ascending"),
True,
GObject.ParamFlags.READWRITE),
"quantize": (float,
_("_Quantization"),
_("Quantization"),
0.0, 1.0, 0.0,
GObject.ParamFlags.READWRITE),
"pchannel": (int,
_("_Partitioning channel"),
"Partitioning channel: " + str(AVAILABLE_CHANNELS),
0, len(AVAILABLE_CHANNELS), 3,
GObject.ParamFlags.READWRITE),
"pquantize": (float,
_("Partition q_uantization"),
_("Partition quantization"),
0.0, 1.0, 0.0,
GObject.ParamFlags.READWRITE),
# Returned value
"new_palette": (Pika.Palette,
_("Palette"),
_("Palette"),
GObject.ParamFlags.READWRITE),
}
## PikaPlugIn virtual methods ##
def do_set_i18n(self, procname):
return True, 'pika30-python', None
def do_query_procedures(self):
return ["python-fu-palette-sort"]
def do_create_procedure(self, name):
procedure = None
if name == "python-fu-palette-sort":
procedure = Pika.Procedure.new(self, name,
Pika.PDBProcType.PLUGIN,
self.run, None)
procedure.set_menu_label(_("_Sort Palette..."))
procedure.set_documentation(
_("Sort the colors in a palette"),
help_doc,
""
)
procedure.set_attribution("João S. O. Bueno, Carol Spears, David Gowers",
"João S. O. Bueno, Carol Spears, David Gowers",
"2006-2014")
procedure.add_menu_path ('<Palettes>')
procedure.add_argument_from_property(self, "run-mode")
procedure.add_argument_from_property(self, "palette")
procedure.add_argument_from_property(self, "selections")
procedure.add_argument_from_property(self, "slice_expr")
procedure.add_argument_from_property(self, "channel1")
procedure.add_argument_from_property(self, "ascending1")
procedure.add_argument_from_property(self, "channel2")
procedure.add_argument_from_property(self, "ascending2")
procedure.add_argument_from_property(self, "quantize")
procedure.add_argument_from_property(self, "pchannel")
procedure.add_argument_from_property(self, "pquantize")
procedure.add_return_value_from_property(self, "new_palette")
return procedure
def run(self, procedure, args, data):
config = procedure.create_config()
config.begin_run(None, Pika.RunMode.INTERACTIVE, args)
run_mode = args.index(0)
palette = args.index(1)
selection = args.index(2)
slice_expr = args.index(3)
channel1 = args.index(4)
ascending1 = args.index(5)
channel2 = args.index(6)
ascending2 = args.index(7)
quantize = args.index(8)
pchannel = args.index(9)
pquantize = args.index(10)
if palette is None or not palette.is_valid():
if palette is not None:
sys.stderr.write(f'Invalid palette id: {palette.get_id()}\n')
sys.stderr.write('This should not happen. Please report to PIKA project.\n')
sys.stderr.write('Falling back to context palette instead.\n')
palette = Pika.context_get_palette()
if not palette.is_valid():
palette_name = palette.get_id()
error = f'Invalid palette id: {palette_name}'
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
GLib.Error(error))
if run_mode == Pika.RunMode.INTERACTIVE:
PikaUi.init('python-fu-palette-sort')
dialog = PikaUi.ProcedureDialog(procedure=procedure, config=config)
dialog.fill (["palette"])
dialog.get_int_combo("selections", PikaUi.IntStore.new (selections_option))
dialog.fill (["selections","slice-expr"])
dialog.get_int_combo("channel1", PikaUi.IntStore.new (AVAILABLE_CHANNELS))
dialog.fill (["channel1", "ascending1"])
dialog.get_int_combo("channel2", PikaUi.IntStore.new (AVAILABLE_CHANNELS))
dialog.fill (["channel2","ascending2", "quantize"])
dialog.get_int_combo("pchannel", PikaUi.IntStore.new (AVAILABLE_CHANNELS))
dialog.fill (["pchannel","pquantize"])
if not dialog.run():
dialog.destroy()
config.end_run(Pika.PDBStatusType.CANCEL)
return procedure.new_return_values(Pika.PDBStatusType.CANCEL, GLib.Error())
else:
palette = config.get_property("palette")
selection = config.get_property("selections")
slice_expr = config.get_property ("slice_expr")
channel1 = config.get_property("channel1")
ascending1 = config.get_property ("ascending1")
channel2 = config.get_property("channel2")
ascending2 = config.get_property ("ascending2")
quantize = config.get_property ("quantize")
pchannel = config.get_property("pchannel")
pquantize = config.get_property ("pquantize")
dialog.destroy()
try:
new_palette = palette_sort(palette, selection, slice_expr, channel1, ascending1,
channel2, ascending2, quantize, pchannel, pquantize)
except ValueError as err:
return procedure.new_return_values(Pika.PDBStatusType.EXECUTION_ERROR,
GLib.Error(str(err)))
config.end_run(Pika.PDBStatusType.SUCCESS)
return_val = procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
value = GObject.Value(Pika.Palette, new_palette)
return_val.remove(1)
return_val.insert(1, value)
return return_val
Pika.main(PaletteSort.__gtype__, sys.argv)

View File

@ -0,0 +1,196 @@
#!/usr/bin/env python3
#
# 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 gi
gi.require_version('Pika', '3.0')
from gi.repository import Pika
gi.require_version('PikaUi', '3.0')
from gi.repository import PikaUi
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import sys
def N_(message): return message
def _(message): return GLib.dgettext(None, message)
def make_gradient(palette, num_segments, num_colors):
# name the gradient same as the source palette
# For now, the name of a resource is the same as the ID
palette_name = palette.get_id()
gradient = Pika.Gradient.new(palette_name)
# assert gradient is valid but is has only one segment
assert gradient.get_number_of_segments() == 1
# split one segment into desired count
# index is zero-based
gradient.segment_range_split_uniform( 0, 0, 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 = palette.entry_get_color(color_number)
_, color_right = palette.entry_get_color(color_number_next)
gradient.segment_set_left_color( color_number, color_left, 100.0)
gradient.segment_set_right_color(color_number, color_right, 100.0)
# Side effects on the context. Probably not what most would expect.
Pika.context_set_gradient(gradient)
return gradient
def run(procedure, args, data):
config = procedure.create_config()
config.begin_run(None, Pika.RunMode.INTERACTIVE, args)
# Get the parameters
run_mode = args.index(0)
palette = args.index(1)
if run_mode == Pika.RunMode.INTERACTIVE:
PikaUi.init(procedure.get_name())
dialog = PikaUi.ProcedureDialog(procedure=procedure, config=config)
# Add palette button
dialog.fill (["palette"])
if not dialog.run():
dialog.destroy()
config.end_run(Pika.PDBStatusType.CANCEL)
return procedure.new_return_values(Pika.PDBStatusType.CANCEL, GLib.Error())
else:
palette = config.get_property("palette")
dialog.destroy()
if palette is None or not palette.is_valid():
if palette is not None:
sys.stderr.write(f'Invalid palette id: {palette.get_id()}\n')
sys.stderr.write('This should not happen. Please report to PIKA project.\n')
sys.stderr.write('Falling back to context palette instead.\n')
palette = Pika.context_get_palette()
num_colors = palette.get_color_count()
if procedure.get_name() == 'python-fu-palette-to-gradient':
num_segments = num_colors - 1
else: # 'python-fu-palette-to-gradient-repeating'
num_segments = num_colors
gradient = make_gradient(palette, num_segments, num_colors)
config.end_run(Pika.PDBStatusType.SUCCESS)
# XXX: for the error parameter, we want to return None.
# Unfortunately even though the argument is (nullable), pygobject
# looks like it may have a bug. So workaround is to just set a
# generic GLib.Error() since anyway the error won't be process with
# PIKA_PDB_SUCCESS status.
# See pygobject#351
retval = procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
# XXX: I don't try to get the GValue with retval.index(1) because it
# actually return a string (cf. pygobject#353). Just create a new
# GValue and replace the default one with this one.
# This comment dates from when resources are strings:
# value = GObject.Value(GObject.TYPE_STRING, gradient)
value = GObject.Value(Pika.Gradient, gradient)
retval.remove(1)
retval.insert(1, value)
return retval
class PaletteToGradient (Pika.PlugIn):
## Parameter: run mode ##
@GObject.Property(type=Pika.RunMode,
default=Pika.RunMode.NONINTERACTIVE,
nick="Run mode", blurb="The run mode")
def run_mode(self):
'''The run mode (unused)'''
return self.runmode
@run_mode.setter
def run_mode(self, runmode):
self.runmode = runmode
## Parameter: palette ##
@GObject.Property(type=Pika.Palette,
default=None,
nick= _("Palette"))
def palette(self):
'''Palette or None for the currently selected palette'''
return self.palette
@palette.setter
def palette(self, palette):
self.palette = palette
## Properties: return values ##
@GObject.Property(type=Pika.Gradient,
default="",
nick=_("The newly created gradient"),
blurb=_("The newly created gradient"))
def new_gradient(self):
"""Read-write integer property."""
return self.new_gradient
@new_gradient.setter
def new_gradient(self, new_gradient):
self.new_gradient = new_gradient
## PikaPlugIn virtual methods ##
def do_set_i18n(self, procname):
return True, 'pika30-python', None
def do_query_procedures(self):
return ['python-fu-palette-to-gradient',
'python-fu-palette-to-gradient-repeating']
def do_create_procedure(self, name):
procedure = Pika.Procedure.new(self, name,
Pika.PDBProcType.PLUGIN,
run, None)
if name == 'python-fu-palette-to-gradient':
procedure.set_menu_label(_("Palette to _Gradient"))
procedure.set_documentation(_("Create a gradient using colors from the palette"),
_("Create a new gradient using colors from the palette."),
"")
elif name == 'python-fu-palette-to-gradient-repeating':
procedure.set_menu_label(_("Palette to _Repeating Gradient"))
procedure.set_documentation(_("Create a repeating gradient using colors from the palette"),
_("Create a new repeating gradient using colors from the palette."),
"")
else:
procedure = None
if procedure is not None:
procedure.set_attribution("Carol Spears, reproduced from previous work by Adrian Likins and Jeff Trefftz",
"Carol Spears", "2006")
# We don't build a GParamSpec ourselves because passing it
# around is apparently broken in Python. Hence this trick.
# See pygobject#227
procedure.add_argument_from_property(self, "run-mode")
procedure.add_argument_from_property(self, "palette")
procedure.add_return_value_from_property(self, "new-gradient")
procedure.add_menu_path ('<Palettes>')
return procedure
Pika.main(PaletteToGradient.__gtype__, sys.argv)

View File

@ -0,0 +1,5 @@
python=@PYTHON_PATH@
python3=@PYTHON_PATH@
/usr/bin/python=@PYTHON_PATH@
/usr/bin/python3=@PYTHON_PATH@
:Python:E::py::python3:

View File

@ -0,0 +1,5 @@
plugins += {
'name': 'python-console',
'srcs': [ 'python-console/pyconsole.py', 'python-console/python-console.py' ],
}

View File

@ -0,0 +1,765 @@
#
# 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 gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
gi.require_version('Gdk', '3.0')
from gi.repository import Gdk
from gi.repository import GObject
from gi.repository import Pango
import code
import sys
import keyword
import re
def pango_pixels(value):
# The PANGO_PIXELS macro is not accessible through GObject
# Introspection. Just reimplement it:
# #define PANGO_PIXELS(d) (((int)(d) + 512) >> 10)
return (value + 512) >> 10
# 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)
def flush(self):
# The output is just a GtkTextBuffer inside a GtkTexView so I
# believe it should be always in-sync without needing to be flushed.
# Nevertheless someone might be just copy-pasting plug-in code to
# test it, for instance. So let's just add a no-op flush() method to
# get a similar interface as the real sys.stdout which we overrode.
# It avoids useless and unexpected code failure.
pass
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.ptr in self.edited:
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.WrapMode.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, False, 0.0, 0.0)
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, False, 0.0, 0.0)
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):
if not self.in_raw_input:
return Gtk.TextView.do_key_press_event(self, event)
tab_pressed = self.tab_pressed
self.tab_pressed = 0
handled = True
state = event.state & (Gdk.ModifierType.SHIFT_MASK |
Gdk.ModifierType.CONTROL_MASK |
Gdk.ModifierType.MOD1_MASK)
keyval = event.keyval
if not state:
if keyval == Gdk.KEY_Escape:
pass
elif keyval == Gdk.KEY_Return:
self._commit()
elif keyval == Gdk.KEY_Up:
self.__history(-1)
elif keyval == Gdk.KEY_Down:
self.__history(1)
elif keyval == Gdk.KEY_Left:
self.__move_cursor(-1)
elif keyval == Gdk.KEY_Right:
self.__move_cursor(1)
elif keyval == Gdk.KEY_Home:
self.__move_cursor(-10000)
elif keyval == Gdk.KEY_End:
self.__move_cursor(10000)
elif keyval == Gdk.KEY_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.ModifierType.CONTROL_MASK:
if keyval == Gdk.KEY_u:
start = self.__get_start()
end = self.__get_cursor()
self.__delete(start, end)
elif keyval == Gdk.KEY_d:
if self.quit_func:
self.quit_func()
else:
handled = False
else:
handled = False
# Handle ordinary keys
if not handled:
return Gtk.TextView.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, False, 0.0, 0.0)
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.get_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()
allocation = Gtk.Widget.get_allocation(self)
return 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(int(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 = int(col_width - len(completions[int(ind)]))
self.__insert(iter, completions[int(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, False, 0.0, 0.0)
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 builtins", self.locals)
#self.locals['builtins'].__dict__['raw_input'] = lambda text='': self.modal_raw_input(text)
self.locals['builtins'].__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 += 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__", 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)
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,)),
}
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)
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.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
window.add(swin)
console = Console(banner="Hello there!",
use_rlcompleter=False,
start_script="gi.require_version('Pika', '3.0')\nfrom gi.repository import Pika\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] != '-pika':
_make_window()

View File

@ -0,0 +1,370 @@
#!/usr/bin/env python3
# Pika-Python - allows the writing of Pika 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 gi
gi.require_version('Pika', '3.0')
gi.require_version('PikaUi', '3.0')
from gi.repository import Pika
from gi.repository import PikaUi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import GLib
import sys
import pyconsole
#import pikashelf, pikaui, pyconsole
import gettext
textdomain = "pika30-python"
gettext.bindtextdomain(textdomain, Pika.locale_directory())
gettext.textdomain(textdomain)
_ = gettext.gettext
PROC_NAME = 'python-fu-console'
RESPONSE_BROWSE, RESPONSE_CLEAR, RESPONSE_SAVE = range(3)
def run(procedure, args, data):
PikaUi.init ("python-console.py")
namespace = {'__builtins__': __builtins__,
'__name__': '__main__', '__doc__': None,
'Babl': gi.repository.Babl,
'cairo': gi.repository.cairo,
'Gdk': gi.repository.Gdk,
'Gegl': gi.repository.Gegl,
'Pika': gi.repository.Pika,
'Gio': gi.repository.Gio,
'Gtk': gi.repository.Gtk,
'GdkPixbuf': gi.repository.GdkPixbuf,
'GLib': gi.repository.GLib,
'GObject': gi.repository.GObject,
'Pango': gi.repository.Pango }
class PikaConsole(pyconsole.Console):
def __init__(self, quit_func=None):
banner = ('PIKA %s Python Console\nPython %s\n' %
(Pika.version(), sys.version))
pyconsole.Console.__init__(self,
locals=namespace, banner=banner,
quit_func=quit_func)
def _commit(self):
pyconsole.Console._commit(self)
Pika.displays_flush()
class ConsoleDialog(PikaUi.Dialog):
def __init__(self):
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
PikaUi.Dialog.__init__(self, use_header_bar=use_header_bar)
self.set_property("help-id", PROC_NAME)
Gtk.Window.set_title(self, _("Python Console"))
Gtk.Window.set_role(self, PROC_NAME)
Gtk.Dialog.add_button(self, _("_Save"), Gtk.ResponseType.OK)
Gtk.Dialog.add_button(self, _("Cl_ear"), RESPONSE_CLEAR)
Gtk.Dialog.add_button(self, _("_Browse..."), RESPONSE_BROWSE)
Gtk.Dialog.add_button(self, _("_Close"), Gtk.ResponseType.CLOSE)
Gtk.Widget.set_name (self, PROC_NAME)
PikaUi.Dialog.set_alternative_button_order_from_array(self,
[ Gtk.ResponseType.CLOSE,
RESPONSE_BROWSE,
RESPONSE_CLEAR,
Gtk.ResponseType.OK ])
self.cons = PikaConsole(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(homogeneous=False, spacing=12)
vbox.set_border_width(12)
contents_area = Gtk.Dialog.get_content_area(self)
contents_area.pack_start(vbox, True, True, 0)
scrl_win = Gtk.ScrolledWindow()
scrl_win.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
vbox.pack_start(scrl_win, True, True, 0)
scrl_win.add(self.cons)
width, height = self.cons.get_default_size()
minreq, requisition = Gtk.Widget.get_preferred_size(scrl_win.get_vscrollbar())
sb_width = requisition.width
sb_height = requisition.height
# 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.
Gtk.Window.set_default_size(self, width + sb_width + 2 * 12, height)
def style_set(self, old_style, user_data):
pass
#style = Gtk.Widget.get_style (self)
#self.cons.stdout_tag.set_property ("foreground", style.text[Gtk.StateType.NORMAL])
#self.cons.stderr_tag.set_property ("foreground", style.text[Gtk.StateType.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 == Gtk.ResponseType.OK:
self.save_dialog()
else:
Gtk.main_quit()
self.cons.grab_focus()
def command_for_procedure(self, proc_name):
'''
Assemble string of Python code that when eval'd
will call proc_name with contrived arguments.
The purpose is to generate a template for a call
to the PDB procedure the user has selected.
The call MIGHT work as is.
Otherwise, the names of the arguments might be enough
that the user can figure out how to edit the template.
The code will run in the environment of the console/browser,
which is not the PIKA v2 PikaFu environment
but the PIKA v3 PyGObject introspected environment.
If ever PikaFu module is resurrected, and Python console imports it,
then revert this code to its v2 form.
'''
proc = Pika.get_pdb().lookup_procedure(proc_name)
if proc is None:
return None
cmd = ''
return_values = proc.get_return_values()
# assert is list of GParamSpec
'''
Cat str of variable names to which unpack return values
Variable names same as return value name, mangled
Str like: 'retval_1, ret_val_2 = '
'''
if len(return_values) > 0:
cmd += ', '.join(x.name.replace('-', '_') for x in return_values)
cmd += ' = '
# else is a void PDB procedure
'''
Cat prefix of str for a call to procedure name
Prefix like: Pika.get_pdb().run_procedure('<foo>',
Note:
- proc name is quoted, run_procedure wants a string.
- proc name has hyphens. Not a Python name. Matches name in PDB.
- trailing comma, another arg to follow:
run_procedure takes two args: string name, and GValueArray of args
'''
cmd += f"Pika.get_pdb().run_procedure('{proc_name}', "
'''
Assemble argument string.
Using names of formal args, which might not match names already
defined in the browsing environment (the REPL).
Args are passed to a PDB procedure in a GValueArray.
Assemble a string like '[arg_1, arg_2]'.
When eval'd, the Python binding will convert to a GValueArray.
'''
param_specs = proc.get_arguments()
cmd += '[ '
'''
Special handling for run mode.
PIKA v2: PikaFu had different handling for run mode.
Insure run mode interactive, i.e. called procedure may open a GUI.
This assumes that procedures use the same formal name for runmode arg.
There might be rare other cases, especially for third party plugins?
E.G. See formal signature of file-gex-load
There is no other way to distinguish the run mode formal argument,
as its formal type is PikaParamEnum, a generic enum.
'''
if len(param_specs) > 0 and param_specs[0].name == 'run-mode':
cmd += 'Pika.RunMode.INTERACTIVE, '
param_specs = param_specs[1:]
# else doesn't take a run mode arg
# Cat string of arg names to a call
# Like: 'arg_1, arg_2' where formal names arg-1 and arg-2
cmd += ', '.join(x.name.replace('-', '_') for x in param_specs)
# terminate the arg array, and close parens the call
cmd += '])'
return cmd
def browse_response(self, dlg, response_id):
if response_id != Gtk.ResponseType.APPLY:
Gtk.Widget.hide(dlg)
return
proc_name = dlg.get_selected()
if not proc_name:
# Apply button was enabled without a selection?
return
cmd = self.command_for_procedure(proc_name)
if cmd is None:
# Should not happen. We browsed a name not in the PDB?
return
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)
# not insert a newline, user can edit and then "enter" the command
def browse(self):
if not self.browse_dlg:
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
dlg = PikaUi.ProcBrowserDialog(use_header_bar=use_header_bar)
Gtk.Window.set_title(dlg, _("Python Procedure Browser"))
Gtk.Window.set_role(dlg, PROC_NAME)
Gtk.Dialog.add_button(dlg, _("_Apply"), Gtk.ResponseType.APPLY)
Gtk.Dialog.add_button(dlg, _("_Close"), Gtk.ResponseType.CLOSE)
Gtk.Dialog.set_default_response(self, Gtk.ResponseType.OK)
PikaUi.Dialog.set_alternative_button_order_from_array(dlg,
[ Gtk.ResponseType.CLOSE,
Gtk.ResponseType.APPLY ])
dlg.connect('response', self.browse_response)
dlg.connect('row-activated',
lambda dlg: dlg.response(Gtk.ResponseType.APPLY))
self.browse_dlg = dlg
Gtk.Window.present(self.browse_dlg)
def save_response(self, dlg, response_id):
if response_id == Gtk.ResponseType.DELETE_EVENT:
self.save_dlg = None
return
elif response_id == Gtk.ResponseType.OK:
filename = dlg.get_filename()
try:
logfile = open(filename, 'w')
except IOError as e:
Pika.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 as e:
Pika.message(_("Could not write to '%s': %s") %
(filename, e.strerror))
return
Gtk.Widget.hide(dlg)
def save_dialog(self):
if not self.save_dlg:
dlg = Gtk.FileChooserDialog()
Gtk.Window.set_title(dlg, _("Save Python-Fu Console Output"))
Gtk.Window.set_transient_for(dlg, self)
Gtk.Dialog.add_button(dlg, _("_Cancel"), Gtk.ResponseType.CANCEL)
Gtk.Dialog.add_button(dlg, _("_Save"), Gtk.ResponseType.OK)
Gtk.Dialog.set_default_response(self, Gtk.ResponseType.OK)
PikaUi.Dialog.set_alternative_button_order_from_array(dlg,
[ Gtk.ResponseType.OK,
Gtk.ResponseType.CANCEL ])
dlg.connect('response', self.save_response)
self.save_dlg = dlg
self.save_dlg.present()
def run(self):
Gtk.Widget.show_all(self)
Gtk.main()
ConsoleDialog().run()
return procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
class PythonConsole (Pika.PlugIn):
## Properties: parameters ##
@GObject.Property(type=Pika.RunMode,
default=Pika.RunMode.NONINTERACTIVE,
nick=_("Run mode"), blurb=_("The run mode"))
def run_mode(self):
"""Read-write integer property."""
return self.runmode
@run_mode.setter
def run_mode(self, runmode):
self.runmode = runmode
## PikaPlugIn virtual methods ##
def do_set_i18n(self, name):
return True, 'pika30-python', None
def do_query_procedures(self):
return [ PROC_NAME ]
def do_create_procedure(self, name):
if name == PROC_NAME:
procedure = Pika.Procedure.new(self, name,
Pika.PDBProcType.PLUGIN,
run, None)
procedure.set_menu_label(_("Python _Console"))
procedure.set_documentation(_("Interactive PIKA Python interpreter"),
_("Type in commands and see results"),
"")
procedure.set_attribution("James Henstridge",
"James Henstridge",
"1997-1999")
procedure.add_argument_from_property(self, "run-mode")
procedure.add_menu_path ("<Image>/Filters/Development/Python-Fu")
return procedure
return None
Pika.main(PythonConsole.__gtype__, sys.argv)

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
# Pika-Python - allows the writing of Pika 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/>.
import gi
gi.require_version('Pika', '3.0')
from gi.repository import Pika
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
import sys
def code_eval(procedure, run_mode, code, args, data):
if code == '-':
code = sys.stdin.read()
exec(code, globals())
return procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
class PythonEval (Pika.PlugIn):
## PikaPlugIn virtual methods ##
def do_set_i18n(self, procname):
return True, 'pika30-python', None
def do_query_procedures(self):
return ['python-fu-eval']
def do_create_procedure(self, name):
procedure = Pika.BatchProcedure.new(self, name, "Python 3",
Pika.PDBProcType.PLUGIN,
code_eval, None)
procedure.set_documentation ("Evaluate Python code",
"Evaluate python code under the python interpreter (primarily for batch mode)",
name)
procedure.set_attribution("Manish Singh",
"Manish Singh",
"2006")
return procedure
Pika.main(PythonEval.__gtype__, sys.argv)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,182 @@
#!/usr/bin/env python3
#
# 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 gi
gi.require_version('Pika', '3.0')
from gi.repository import Pika
gi.require_version('PikaUi', '3.0')
from gi.repository import PikaUi
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
import time
import sys
'''
A Python plugin.
Tests PikaProcedureDialog.
Temporarily, just testing widgets for subclasses of PikaResource.
Temporarily, just testing Brush subclass of Resource.
FUTURE: For all the parameter types.
Formerly PF_ constants, now all parameters for which PikaParamSpecs exist.
Not localized, no i18n
'''
def process_args(brush, font, gradient, palette, pattern):
'''
Test the args are sane.
'''
assert brush is not None
assert isinstance(brush, Pika.Brush)
id = brush.get_id()
name = brush.get_name()
assert id is not None
msg = "Brush: {} (ID: {})".format(name, id)
print(msg)
Pika.message(msg)
assert font is not None
assert isinstance(font, Pika.Font)
id = font.get_id()
name = font.get_name()
assert id is not None
msg = "Font: {} (ID: {})".format(name, id)
print(msg)
Pika.message(msg)
assert gradient is not None
assert isinstance(gradient, Pika.Gradient)
id = gradient.get_id()
name = gradient.get_name()
assert id is not None
msg = "Gradient: {} (ID: {})".format(name, id)
print(msg)
Pika.message(msg)
assert palette is not None
assert isinstance(palette, Pika.Palette)
id = palette.get_id()
name = palette.get_name()
assert id is not None
msg = "Palette: {} (ID: {})".format(name, id)
print(msg)
Pika.message(msg)
assert pattern is not None
assert isinstance(pattern, Pika.Pattern)
id = pattern.get_id()
name = pattern.get_name()
assert id is not None
msg = "Pattern: {} (ID: {})".format(name, id)
print(msg)
Pika.message(msg)
return
def test_dialog(procedure, run_mode, image, n_drawables, drawables, args, data):
'''
Just a standard shell for a plugin.
'''
config = procedure.create_config()
config.begin_run(image, run_mode, args)
if run_mode == Pika.RunMode.INTERACTIVE:
PikaUi.init('python-fu-test-dialog')
dialog = PikaUi.ProcedureDialog(procedure=procedure, config=config)
dialog.fill(None)
if not dialog.run():
dialog.destroy()
config.end_run(Pika.PDBStatusType.CANCEL)
return procedure.new_return_values(Pika.PDBStatusType.CANCEL, GLib.Error())
else:
dialog.destroy()
brush = config.get_property('brush')
font = config.get_property('font')
gradient = config.get_property('gradient')
palette = config.get_property('palette')
pattern = config.get_property('pattern')
Pika.context_push()
process_args(brush, font, gradient, palette, pattern)
Pika.displays_flush()
Pika.context_pop()
config.end_run(Pika.PDBStatusType.SUCCESS)
return procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
class TestDialogPlugin (Pika.PlugIn):
## Parameters ##
# See comments about this in foggify.py, from which we borrowed
brush = GObject.Property(type = Pika.Brush,
nick = "_Brush",
blurb = "Brush")
font = GObject.Property(type = Pika.Font,
nick = "_Font",
blurb = "Font")
gradient = GObject.Property(type = Pika.Gradient,
nick = "_Gradient",
blurb = "Gradient")
palette = GObject.Property(type = Pika.Palette,
nick = "_Palette",
blurb = "Palette")
pattern = GObject.Property(type = Pika.Pattern,
nick = "Pa_ttern",
blurb = "Pattern")
# FUTURE all other Pika classes that have PikaParamSpecs
## PikaPlugIn virtual methods ##
def do_set_i18n(self, procname):
return True, 'pika30-python', None
def do_query_procedures(self):
return [ 'python-fu-test-dialog' ]
def do_create_procedure(self, name):
procedure = Pika.ImageProcedure.new(self, name,
Pika.PDBProcType.PLUGIN,
test_dialog, None)
procedure.set_sensitivity_mask (Pika.ProcedureSensitivityMask.NO_IMAGE)
procedure.set_documentation ("Test dialog",
"Test dialog",
name)
procedure.set_menu_label("Test dialog...")
procedure.set_attribution("Lloyd Konneker",
"Lloyd Konneker",
"2022")
# Top level menu "Test"
procedure.add_menu_path ("<Image>/Test")
procedure.add_argument_from_property(self, "brush")
procedure.add_argument_from_property(self, "font")
procedure.add_argument_from_property(self, "gradient")
procedure.add_argument_from_property(self, "palette")
procedure.add_argument_from_property(self, "pattern")
return procedure
Pika.main(TestDialogPlugin.__gtype__, sys.argv)