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()