Skip to content
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
Cannot retrieve contributors at this time
executable file 486 lines (418 sloc) 16.6 KB
#!/usr/bin/env python
# This is a plug-in intended to be bound to Ctrl-S, and to act as
# a combination of Save, Save as..., Overwrite, and Export...
# It saves the image (whether its type would normally require Saving
# or Exporting), and optionally allows for a copy of the image to be
# exported to another file, possibly also scaled differently.
# The copy must be saved to the same directory as the original
# (because juggling two file choosers would make the dialog too huge
# to fit on most screens!)
# Regardless of image format chosen, the image will be marked clean
# so you won't get prompted for an unsaved image at quit or close time.
# The first time you save a file, you should be prompted for filename
# (even if you opened an existing file).
# After that, the plug-in will remember the filename you set.
# You can always change it with Saver as...
# So you can have your XCF with high resolution and all layers,
# and automatically export to a quarter-scale JPEG copy every time
# you save the original. Or you can just overwrite your JPG if
# all you're doing is a quick edit.
# The basic save filename follows img.filename.
# The copy, if any, is saved in a parasite:
# export-scaled: filename\npercent\nwidth\nheight
# This plug-in is hosted at
# Copyright 2013,2014 by Akkana Peck,
# You may use and distribute this plug-in under the terms of the GPL v2
# or, at your option, any later GPL version.
from gimpfu import *
import gtk
import os
import collections
# Image original size
orig_width = 0
orig_height = 0
# The three gtk.Entry widgets we'll monitor:
percent_e = None
width_e = None
height_e = None
# A way to show errors to the user:
warning_label = None
# A way to turn off notifications while we're calculating
busy = False
def save_both(img, drawable, filename, copyname, width, height):
'''Save the image, and also save the copy if appropriate,
doing any duplicating or scaling that might be necessary.
Returns None on success, else a nonempty string error message.
msg = "Saving " + filename
if copyname:
if width and height:
msg += " and a scaled copy, " + copyname
msg += " and a copy, " + copyname
print msg
# Don't use gimp_message -- it can pop up a dialog.
# pdb.gimp_message_set_handler(MESSAGE_BOX)
# pdb.gimp_message(msg)
# Alternately, could use gimp_progress, though that's not ideal.
# If you use gimp_progress, don't forget to call pdb.gimp_progress_end()
# Is there any point to pushing and popping the context?
# Set up the parasite with information about the copied image,
# so it will be saved with the main XCF image.
if copyname:
if (width and height):
percent = width * 100.0 / img.width
# XXX note that this does not guard against changed aspect ratios.
parastring = '%s\n%d\n%d\n%d' % (copyname, percent, width, height)
parastring = '%s\n100.0\nNone\nNone' % (copyname)
print "Saving parasite", parastring
para = img.attach_new_parasite('export-copy', 1, parastring)
# The second argument is is_persistent
# Also make sure that copyname is a full path (if filename is).
copypath = os.path.split(copyname)
if not copypath[0] or len(copypath) == 1:
filepath = os.path.split(filename)
if len(filepath) > 1 and filepath[0]:
print "Turning", copyname, "into a full path:",
copyname = os.path.join(filepath[0], copyname)
print copyname
# Now the parasite is safely attached, and we can save.
# Alas, we can't attach the JPEG settings parasite until after
# we've saved the copy, so that won't get saved with the main image
# though it will be attached to the image and remembered in
# this session, and the next time we save it should be remembered.
def is_xcf(thefilename):
base, ext = os.path.splitext(thefilename)
ext = ext.lower()
if ext == '.gz' or ext == '.bz2':
base, ext = os.path.splitext(base)
ext = ext.lower()
return (ext == '.xcf')
# With the new "crop doesn't really crop", gimp_file_save saves the
# whole image, not the cropped version.
# So detect cropping or anything else that might make layers
# be sized differently from the image.
def different_size_layer(img):
for l in img.layers:
if l.width != img.width:
return True
if l.height != img.height:
return True
return False
# First, save the original image.
if is_xcf(filename) or (len(img.layers) < 2
and not different_size_layer(img)):
pdb.gimp_file_save(img, drawable, filename, filename)
# It's not XCF and it has multiple layers,
# or one layer that's been cropped.
# We need to make a new image and flatten it.
copyimg = pdb.gimp_image_duplicate(img)
# Don't actually flatten since that will prevent saving transparent png.
pdb.gimp_image_merge_visible_layers(copyimg, CLIP_TO_IMAGE)
pdb.gimp_file_save(copyimg, copyimg.active_layer, filename, filename)
# We've done the important part, so mark the image clean.
img.filename = filename
# If we don't have to save a copy, return.
if not copyname:
return None
# We'll need a new image if the copy is non-xcf and we have more
# than one layer, or if we're scaling.
if (width and height):
# We're scaling!
copyimg = pdb.gimp_image_duplicate(img)
copyimg.scale(width, height)
print "Scaling to", width, 'x', height
print "Set parastring to", parastring, "(end of parastring)"
elif len(img.layers) > 1 and not is_xcf(copyname):
# We're not scaling, but we still need to flatten.
copyimg = pdb.gimp_image_duplicate(img)
# copyimg.flatten()
pdb.gimp_image_merge_visible_layers(copyimg, CLIP_TO_IMAGE)
print "Flattening but not scaling"
copyimg = img
print "Not scaling or flattening"
# gimp-file-save insists on being passed a valid layer,
# even if saving to a multilayer format such as XCF. Go figure.
# I don't get it. What's the rule for whether gimp_file_save
# will prompt for jpg parameters? Saving back to an existing
# file doesn't seem to help.
# run_mode=RUN_WITH_LAST_VALS is supposed to prevent prompting,
# but actually seems to do nothing. Copying the -save-options
# parasites is more effective.
pdb.gimp_file_save(copyimg, copyimg.active_layer, copyname, copyname,
except RuntimeError, e:
print "Runtime error -- didn't save"
return "Runtime error -- didn't save"
# Find any image type settings parasites (e.g. jpeg settings)
# that got set during save, so we'll be able to use them
# next time.
def copy_settings_parasites(fromimg, toimg):
for pname in fromimg.parasite_list():
if pname[-9:] == '-settings' or pname[-13:] == '-save-options':
para = fromimg.parasite_find(pname)
if para:
toimg.attach_new_parasite(pname, para.flags,
# Copy any settings parasites we may have saved from previous runs:
copy_settings_parasites(copyimg, img)
return None
def init_from_parasite(img):
'''Returns copyname, percent, width, height.'''
para = img.parasite_find('export-copy')
if para:
#copyname, percent, width, height = \
paravals = \
map(lambda x: 0 if x == 'None' or x == '' else x,'\n'))
copyname = paravals[0]
percent = float(paravals[1])
width = int(paravals[2])
height = int(paravals[3])
print "Read parasite values", copyname, percent, width, height
return copyname, percent, width, height
return None, 100.0, img.width, img.height
def python_fu_saver(img, drawable):
# Figure out whether this image already has filenames chosen;
# if so, just save and export;
# if not, call python_fu_saver_as(img, drawable).
# Figure out whether there's an export parasite
# so we can pass width and height to save_both.
copyname, export_percent, export_width, export_height = \
if img.filename:
save_both(img, drawable, img.filename, copyname,
export_width, export_height)
# If we don't have a filename, either from the current session
# or saved as a parasite, then we'd better prompt for one.
python_fu_saver_as(img, drawable)
def python_fu_saver_as(img, drawable):
global percent_e, width_e, height_e, orig_width, orig_height, warning_label
orig_filename = img.filename
orig_width = img.width
orig_height = img.height
# Do we already have defaults?
copyname, export_percent, export_width, export_height = \
chooser = gtk.FileChooserDialog(title=None,
# Set a current folder, since otherwise it'll be empty
# and will nag the user to set one.
if img.filename:
# No directory set yet. So pick a likely directory
# that's used by a lot of images currently open.
counts = collections.Counter()
for i in gimp.image_list() :
if i.filename :
counts[os.path.dirname(i.filename)] += 1
try :
common = counts.most_common(1)[0][0]
except :
# GIMP has no images open with pathnames associated.
# So we can't guess at a directory; just leave it blank.
copybox = gtk.Table(rows=3, columns=7)
l = gtk.Label("Export a copy to:")
l.set_alignment(1.0, 0.5) # Right align
copybox.attach(l, 0, 1, 0, 1,
xpadding=5, ypadding=5)
copyname_e = gtk.Entry()
if copyname:
copybox.attach(copyname_e, 1, 7, 0, 1,
xpadding=5, ypadding=5)
l = gtk.Label("Scale the copy:")
l.set_alignment(1.0, 0.5)
copybox.attach(l, 0, 1, 1, 2,
xpadding=5, ypadding=5)
l = gtk.Label("Percent:")
l.set_alignment(1.0, 0.5)
copybox.attach(l, 1, 2, 1, 2,
xpadding=5, ypadding=5)
adj = gtk.Adjustment(int(export_percent), 1, 10000, 1, 10, 0)
percent_e = gtk.SpinButton(adj, 0, 0)
percent_e.connect("changed", entry_changed, 'p');
copybox.attach(percent_e, 2, 3, 1, 2,
xpadding=5, ypadding=5)
l = gtk.Label("Width:")
l.set_alignment(1.0, 0.5)
copybox.attach(l, 3, 4, 1, 2,
xpadding=5, ypadding=5)
adj = gtk.Adjustment(int(export_width), 1, 10000, 1, 10, 0)
width_e = gtk.SpinButton(adj, 0, 0)
width_e.connect("changed", entry_changed, 'w');
copybox.attach(width_e, 4, 5, 1, 2,
xpadding=5, ypadding=5)
l = gtk.Label("Height:")
l.set_alignment(1.0, 0.5)
copybox.attach(l, 5, 6, 1, 2,
xpadding=5, ypadding=5)
adj = gtk.Adjustment(int(export_height), 1, 10000, 1, 10, 0)
height_e = gtk.SpinButton(adj, 0, 0)
height_e.connect("changed", entry_changed, 'h');
copybox.attach(height_e, 6, 7, 1, 2,
xpadding=5, ypadding=5)
warning_label = gtk.Label("")
copybox.attach(warning_label, 0, 7, 2, 3,
xpadding=5, ypadding=5)
# Oh, cool, we could have shortcuts to image folders,
# and maybe remove the stupid fstab shortcuts GTK adds for us.
# Loop to catch errors/warnings:
while True:
response =
if response != gtk.RESPONSE_OK:
percent = float(percent_e.get_text())
except ValueError:
percent = None
width = int(width_e.get_text())
except ValueError:
width = None
height = int(height_e.get_text())
except ValueError:
height = None
filename = chooser.get_filename()
copyname = copyname_e.get_text()
if copyname == orig_filename:
warning_label.set_text("Change the name or the directory -- don't overwrite original file!")
# Whew, it's not the original filename, so now we can save.
dpy = chooser.get_display()
err = save_both(img, drawable, filename, copyname, width, height)
if err:
markup = '<span foreground="red" size="larger" weight=\"bold">'
markup_end = '</span>'
warning_label.set_markup(markup + "Didn't save to " + filename
+ ":" + str(e) + markup_end)
print "Error:", err
# Otherwise, save_both was happy so we can continue.
# Doesn't work to compare entry against percent_e, etc.
# Must pass that info in a separate arg, darnit.
def entry_changed(entry, which):
global percent_e, width_e, height_e, orig_width, orig_height, busy
if busy:
# Can't use get_value() or get_value_as_int() because they
# don't work before an update(), but update() will usually
# overwrite what the user typed. So define a function:
def get_num_value(spinbox):
s = spinbox.get_text()
if not s:
return 0
p = int(s)
return p
except ValueError:
p = float(s)
return p
except ValueError:
return 0
if which == 'p':
p = get_num_value(percent_e)
if not p: return
busy = True
w = int(orig_width * p / 100.)
h = int(orig_height * p / 100.)
busy = False
elif which == 'w':
w = get_num_value(width_e)
if not w: return
busy = True
p = w * 100. / orig_width
h = int(orig_height * p / 100.)
busy = False
elif which == 'h':
h = get_num_value(height_e)
if not h: return
busy = True
p = h * 100. / orig_height
w = int(orig_width * p / 100.)
busy = False
print "I'm confused -- not width, height or percentage"
"Save or export the current image, optionally also exporting a scaled version, prompting for filename only if needed.",
"Save or export the current image, optionally also exporting a scaled version, prompting for filename only if needed.",
"Akkana Peck",
"Akkana Peck",
(PF_IMAGE, "image", "Input image", None),
(PF_DRAWABLE, "drawable", "Input drawable", None),
menu = "<Image>/File/Save/"
"Prompt to save or export the current image, optionally also exporting a scaled version.",
"Prompt to save or export the current image, optionally also exporting a scaled version.",
"Akkana Peck",
"Akkana Peck",
"Saver as...",
(PF_IMAGE, "image", "Input image", None),
(PF_DRAWABLE, "drawable", "Input drawable", None),
menu = "<Image>/File/Save/"