File: edge_fader.py
#!/usr/bin/env python
"""
GDP Image Edge Fader Proof of Concept
--------------------------------------
SegPhault (Ryan Paul) - 08/01/2006
This utility automatically blurs the edges of images and applies a drop shadow.
The graphical interface was implemented to facilitate dynamic alteration of the
attributes used by the edge fading proccess. It will make it easy to compare
various configurations and determine exactly which values should be used by
default for documentation screenshots.
There are three values exposed to the interface:
o border - specifies the size of the faded regions
o filled - specifies how far from the edge the fades should end
o offset - specifies the the drop shadow offset
Dependencies
------------
o Python bindings for Cairo
o Python bindings for GTK
o PIL
Issues
------
The edge fading is implemented in Cairo, which does not support gaussian
blur filtering yet. The shadow can't be implemented without that, so I
ended doing that part with PIL.
Unfortunately, version 1.0.2 of PyCairo doesn't have any mechanism for
outputting raw image data that PIL can read. The CVS version has a nifty
surface method called to_rgba that does what I want, but for now I have to
save the image from the Cairo surface to disk and then load it back in with
PIL.
Resources
---------
Python Cookbook recipe for guassian blur drop shadows with PIL:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/474116
Example of how to access Cairo data in PIL:
http://webcvs.cairographics.org/pycairo/test/to_rgba.py?view=markup
Loading PIL data into a GTK Pixbuf:
http://www.daa.com.au/pipermail/pygtk/2003-June/005268.html
"""
import sys, cairo, gtk, StringIO
from PIL import Image, ImageFilter
TEMP_FILE = "/tmp/junk.png"
def pil_to_gdk(img):
file = StringIO.StringIO()
img.save(file, 'ppm')
contents = file.getvalue()
file.close()
loader = gtk.gdk.PixbufLoader('pnm')
loader.write (contents, len(contents))
pixbuf = loader.get_pixbuf()
loader.close()
return pixbuf
def fade_edges(img_file, border = 30, filled = 1, offset = 6,
show_shadow = True, fade_edges = True):
img = cairo.ImageSurface.create_from_png(img_file)
width, height = img.get_width(), img.get_height()
c = cairo.Context(img)
ops = ((0, filled, 0, border), # top
(filled, 0, border, 0), # left
(0, height - filled, 0, height - border), # bottom
(width - filled, 0, width - border, 0)) # right
if fade_edges:
for op in ops:
p = cairo.LinearGradient(*op)
p.add_color_stop_rgba(0,1,1,1,1)
p.add_color_stop_rgba(1,1,1,1,0)
c.rectangle(0,0, width, height)
c.set_source(p); c.fill_preserve()
img.write_to_png(TEMP_FILE)
image = Image.open(TEMP_FILE)
if not show_shadow: return image
back = Image.new(image.mode,
(width + offset * 3, height + offset * 3), "rgb(255,255,255)")
back.paste("rgb(68,68,68)", [
offset, offset * 2, offset + width, offset + height])
for x in range(3): back = back.filter(ImageFilter.BLUR)
back.paste(image, (0, 0))
return back
def save_to_disk(pil_img, target_file):
pil_to_gdk(pil_img).save(target_file, "png")
## The rest of this script contains a user interface for testing purposes ##
class EdgeFadeExperiment(gtk.Window):
def __init__(self, img_file, **args):
gtk.Window.__init__(self)
self.connect("destroy", self.on_close)
self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("white"))
layout = gtk.VBox(False); self.add(layout)
self.image = gtk.Image(); layout.add(self.image)
self.image_file = img_file
self.sliders = dict([(n, v) for n, v in self.add_sliders(**args)])
for n, v in self.sliders.items():
v.set_digits(0)
hb = gtk.HBox(False)
hb.pack_start(gtk.Label(n.capitalize() + ":"), False, False, 10)
hb.add(v); layout.add(hb)
self.optInstant = gtk.CheckButton("Instant _apply", True)
self.optShadow = gtk.CheckButton("Render _shadow", True)
self.optFade = gtk.CheckButton("Fade _edges", True)
self.optShadow.set_active(True); self.optFade.set_active(True)
btnUpdate = gtk.Button("_Update"); btnSave = gtk.Button("_Save")
btnUpdate.connect("pressed", lambda *w: self.render_image(True))
btnSave.connect("pressed", self.on_save)
hb = gtk.HBox(False); layout.add(hb)
hb.add(self.optInstant); hb.add(self.optShadow); hb.add(self.optFade)
hb.pack_end(btnUpdate, False, False); hb.pack_end(btnSave, False, False)
def add_sliders(self, **args):
for n, v in args.items():
adj = gtk.Adjustment(v, 0, 100, 1)
adj.connect("value-changed", lambda *a: self.render_image())
yield n, gtk.HScale(adj)
def render_image(self, update = False):
if self.optInstant.get_active() or update:
args = dict([(n, int(v.get_adjustment().value)) for n, v in self.sliders.items()])
args["show_shadow"] = self.optShadow.get_active()
args["fade_edges"] = self.optFade.get_active()
self.image.set_from_pixbuf(pil_to_gdk(fade_edges(self.image_file, **args)))
def on_save(self, *args):
buttons = (gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK)
d = gtk.FileChooserDialog("Save file", self, gtk.FILE_CHOOSER_ACTION_SAVE, buttons)
if d.run() == gtk.RESPONSE_OK:
self.image.get_pixbuf().save(d.get_filename(), "png")
d.destroy()
def on_close(self, *args):
gtk.main_quit()
if __name__ == '__main__':
w = EdgeFadeExperiment(sys.argv[1], border = 30, filled = 1, offset = 6)
w.render_image(True)
w.show_all()
gtk.main()