Permalink
Cannot retrieve contributors at this time
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?
gimp-plugins/saver.py
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
executable file
486 lines (418 sloc)
16.6 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 https://github.com/akkana/gimp-plugins/blob/master/saver.py | |
# Copyright 2013,2014 by Akkana Peck, http://www.shallowsky.com/software/ | |
# 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 | |
else: | |
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() | |
#pdb.gimp_progress_set_text(msg) | |
# Is there any point to pushing and popping the context? | |
#gimp.context_pop() | |
# 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) | |
else: | |
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) | |
else: | |
# 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. | |
#copyimg.flatten() | |
pdb.gimp_image_merge_visible_layers(copyimg, CLIP_TO_IMAGE) | |
pdb.gimp_file_save(copyimg, copyimg.active_layer, filename, filename) | |
gimp.delete(copyimg) | |
# We've done the important part, so mark the image clean. | |
img.filename = filename | |
pdb.gimp_image_clean_all(img) | |
# 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" | |
else: | |
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. | |
try: | |
pdb.gimp_file_save(copyimg, copyimg.active_layer, copyname, copyname, | |
run_mode=RUN_WITH_LAST_VALS) | |
except RuntimeError, e: | |
gimp.delete(copyimg) | |
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, para.data) | |
# Copy any settings parasites we may have saved from previous runs: | |
copy_settings_parasites(copyimg, img) | |
gimp.delete(copyimg) | |
#gimp.Display(copyimg) | |
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, | |
para.data.split('\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 | |
else: | |
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 = \ | |
init_from_parasite(img) | |
if img.filename: | |
save_both(img, drawable, img.filename, copyname, | |
export_width, export_height) | |
else: | |
# 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 = \ | |
init_from_parasite(img) | |
chooser = gtk.FileChooserDialog(title=None, | |
action=gtk.FILE_CHOOSER_ACTION_SAVE, | |
buttons=(gtk.STOCK_CANCEL, | |
gtk.RESPONSE_CANCEL, | |
gtk.STOCK_SAVE, | |
gtk.RESPONSE_OK)) | |
# Set a current folder, since otherwise it'll be empty | |
# and will nag the user to set one. | |
if img.filename: | |
chooser.set_current_name(os.path.basename(img.filename)) | |
chooser.set_current_folder(os.path.dirname(img.filename)) | |
else: | |
# 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] | |
chooser.set_current_folder(common) | |
except : | |
# GIMP has no images open with pathnames associated. | |
# So we can't guess at a directory; just leave it blank. | |
pass | |
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: | |
copyname_e.set_text(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) | |
copybox.show_all() | |
chooser.set_extra_widget(copybox) | |
# Oh, cool, we could have shortcuts to image folders, | |
# and maybe remove the stupid fstab shortcuts GTK adds for us. | |
#chooser.add_shortcut_folder(folder) | |
#chooser.remove_shortcut_folder(folder) | |
# Loop to catch errors/warnings: | |
while True: | |
response = chooser.run() | |
if response != gtk.RESPONSE_OK: | |
chooser.destroy() | |
return | |
try: | |
percent = float(percent_e.get_text()) | |
except ValueError: | |
percent = None | |
try: | |
width = int(width_e.get_text()) | |
except ValueError: | |
width = None | |
try: | |
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!") | |
continue | |
# Whew, it's not the original filename, so now we can save. | |
dpy = chooser.get_display() | |
chooser.hide() | |
dpy.sync() | |
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) | |
warning_label.set_text(err) | |
print "Error:", err | |
chooser.show() | |
continue | |
else: | |
pdb.gimp_image_clean_all(img) | |
# Otherwise, save_both was happy so we can continue. | |
chooser.destroy() | |
#gimp.context_pop() | |
return | |
# 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: | |
return | |
# 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 | |
try: | |
p = int(s) | |
return p | |
except ValueError: | |
try: | |
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.) | |
width_e.set_text(str(w)) | |
h = int(orig_height * p / 100.) | |
height_e.set_text(str(h)) | |
busy = False | |
elif which == 'w': | |
w = get_num_value(width_e) | |
if not w: return | |
busy = True | |
p = w * 100. / orig_width | |
percent_e.set_text(str(p)) | |
h = int(orig_height * p / 100.) | |
height_e.set_text(str(h)) | |
busy = False | |
elif which == 'h': | |
h = get_num_value(height_e) | |
if not h: return | |
busy = True | |
p = h * 100. / orig_height | |
percent_e.set_text(str(p)) | |
w = int(orig_width * p / 100.) | |
width_e.set_text(str(w)) | |
busy = False | |
else: | |
print "I'm confused -- not width, height or percentage" | |
register( | |
"python_fu_saver", | |
"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", | |
"2014", | |
"Saver", | |
"*", | |
[ | |
(PF_IMAGE, "image", "Input image", None), | |
(PF_DRAWABLE, "drawable", "Input drawable", None), | |
], | |
[], | |
python_fu_saver, | |
menu = "<Image>/File/Save/" | |
) | |
register( | |
"python_fu_saver_as", | |
"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", | |
"2014", | |
"Saver as...", | |
"*", | |
[ | |
(PF_IMAGE, "image", "Input image", None), | |
(PF_DRAWABLE, "drawable", "Input drawable", None), | |
], | |
[], | |
python_fu_saver_as, | |
menu = "<Image>/File/Save/" | |
) | |
main() | |