HOME/Articles/

pil example Django-Jcrop-form (snippet)

Article Outline

Python pil example 'Django-Jcrop-form'

Functions in program:

  • def img_edit_view(request):

Modules used in program:

  • import Image as pil

python Django-Jcrop-form

Python pil example: Django-Jcrop-form

"""
Django Jcrop form

Author: Markus Thielen, mt[aaTt]thiguten.de
Comment: MicFan, fanlinsheng{AATT}gmail.com

http://djangosnippets.org/snippets/2316/

= Description =

Implements a Django form that integrates image uploading plus cropping using
the awesome Jcrop plugin (http://deepliquid.com/content/Jcrop.html).

It does not create or use any models, so to use it, simply copy it to
your project tree and import it as appropriate.

= License =

MIT

= Documention =

http://pillow.readthedocs.org/en/latest/index.html

= Install =

sudo apt-get build-dep python-imaging
sudo apt-get install python-dev
wget -q -O - https://raw.github.com/gist/1225180/patch.sh | sudo bash
sudo ln -sf /usr/lib/i386-linux-gnu/libfreetype.so /usr/lib/
sudo ln -sf /usr/lib/i386-linux-gnu/libz.so /usr/lib/
sudo ln -sf /usr/lib/i386-linux-gnu/libjpeg.so /usr/lib/
pip install -I PIL

    --------------------------------------------------------------------
    PIL 1.1.7 SETUP SUMMARY
    --------------------------------------------------------------------
    version       1.1.7
    platform      linux2 2.7.4 (default, Apr 19 2013, 18:32:33)
                  [GCC 4.7.3]
    --------------------------------------------------------------------
    *** TKINTER support not available (Tcl/Tk 8.5 libraries needed)
    --- JPEG support available
    *** ZLIB (PNG/ZIP) support not available
    --- FREETYPE2 support available
    *** LITTLECMS support not available
    --------------------------------------------------------------------

= Limitations =

Many, probably ;-) It does basically work but lacks proper error handling.
If you upload something that is not an image, no error is dispayed.
I hope I find the time to fix this. If you have a hint, I'd be grateful if you
dropped me a note.

= Usage =

In your views.py, import JcropForm.
The view function that displays the form has three parts (or control flows):

 * if request is POSTed but contains no uploaded files, crop coordinates
   were submitted. Use JcropForm's crop/resize/save methods to apply.
 * if request was posted with file data, the user just uploaded a new
   image. Use JcropForm's static method prepare_uploaded_img to scale the
   image to a reasonable size (that does not break your layout) and save it
 * for normal GET requests just display the form with the current image.


Example view function:

@login_required # the view func expects a logged on user
def img_edit_view(request):
  # get the profile (i.e. the model containing the image to edit);
  # In this example, the model in question is the user profile model,
  # so we can use Django's get_profile() method.
  profile = request.user.get_profile()

  # define a fixed aspect ratio for the user image
  aspect = 105.0 / 75.0
  # the final size of the user image
  final_size = (105, 75)

  if request.method == "POST" and len(request.FILES) == 0:
    # user submitted form with crop coordinates
    form = JcropForm(request.POST)
    if form.is_valid():
      # apply cropping
      form.crop()
      form.resize(final_size)
      form.save()
      # redirect to profile display page
      return HttpResponseRedirect("/myprofile/")

  elif request.method == "POST" and len(request.FILES):
    # user uploaded a new image; save it and make sure it is not too large
    # for our layout
    img_fn = JcropForm.prepare_uploaded_img(request.FILES, image_upload_to, 
                                            profile, (370, 500))
    if img_fn:
      # store new image in the member instance
      profile.avatar = img_fn # 'avatar' is an ImageField
      profile.save()

      # redisplay the form with the new image; this is the same as for
      # GET requests -> fall through to GET

  elif request.method != "GET":
    # only POST and GET, please
    return HttpResponse(status=400)

  # for GET requests, just display the form with current image
  form = JcropForm(initial        = { "imagefile": profile.avatar },
                   jcrop_options  = { 
                                      "aspectRatio":aspect,
                                      "setSelect": "[100, 100, 50, 50]",
                                    }
                  )

  return render_to_response("profile/img_edit.html",
                            {
                              "form": form,
                            },
                            RequestContext(request))


The template is the same as for normal Django forms, nothing special there.

This code is somehow inspired by https://github.com/azizmb/django-ip-form,
although the original code did not work for me.

"""
from django import forms
from django.conf import settings
from django.utils.safestring import mark_safe
from django.utils.datastructures import MultiValueDictKeyError
import Image as pil

UPLOAD_IMG_ID="new-img-file"

class JcropWidget(forms.Widget):
  class Media:
    # form media, i.e. CSS and JavaScript needed for Jcrop.
    # You'll have to adopt these to your project's paths.
    css = {
      'all': (settings.MEDIA_URL + "css/jquery.Jcrop.css",)
    }
    js = (
      settings.MEDIA_URL + "js/lib/jquery.Jcrop.min.js",
    )

  # fixed Jcrop options; to pass options to Jcrop, use the jcrop_options
  # argument passed to the JcropForm constructor. See example above.
  jcrop_options = {
                    "onSelect": "storeCoords", 
                    "onChange": "storeCoords",
                  }

  # HTML template for the widget. 
  #
  # The widget is constructed from the following parts:
  #
  #  * HTML <img> - the actual image used for displaying and cropping
  #  * HTML <label> and <input type="file> - used for uploading a new
  #                                          image
  #  * HTML <input type="hidden"> - to remember image path and filename
  #  * JS code - The JS code makes the image a Jcrop widget and 
  #              registers an event handler for the <input type="file"> 
  #              widget. The event handler submits the form so the new
  #              image is sent to the server without the user having
  #              to press the submit button.
  # 
  markup = """
  <img id="jcrop-img" src="%(MEDIA_URL)s%(img_fn)s"/><br/>
  <label for="new-img-file">Neues Bild hochladen:</label>
  <input type="file" name="%(UPLOAD_IMG_ID)s" id="%(UPLOAD_IMG_ID)s"/>
  <input type="hidden" name="imagefile" id="imagefile" value="%(imagefile)s"/>
  <script type="text/javascript">
  function storeCoords(c)
  {
    jQuery('#id_x1').val(c.x);
    jQuery('#id_x2').val(c.x2);
    jQuery('#id_y1').val(c.y);
    jQuery('#id_y2').val(c.y2);
  }
  jQuery(function() {
      jQuery('#jcrop-img').Jcrop(%(jcrop_options)s);
      jQuery('#%(UPLOAD_IMG_ID)s').change(function(e){
        var form = jQuery('#%(UPLOAD_IMG_ID)s').parents('form:first');
        form.submit();
      });
  });</script>
    """

  def __init__(self, attrs=None):
    """
    __init__ does nothing special for now
    """
    super(JcropWidget, self).__init__(attrs)

  def add_jcrop_options(self, options):
    """
    add jcrop options; options is expected to be a dictionary of name/value
    pairs that Jcrop understands; 
    see http://deepliquid.com/content/Jcrop_Manual.html#Setting_Options
    """
    for k, v in options.items():
      self.jcrop_options[k] = v

  def render(self, name, value, attrs=None):
    """
    render the Jcrop widget in HTML
    """
    # translate jcrop_options dictionary to JavaScipt
    jcrop_options = "{";
    for k, v in self.jcrop_options.items():
      jcrop_options = jcrop_options + "%s: %s," % (k, v)
    jcrop_options = jcrop_options + "}"

    # fill in HTML markup string with actual data
    output = self.markup % {
                             "MEDIA_URL": settings.MEDIA_URL, 
                             "img_fn": str(value),
                             "UPLOAD_IMG_ID": UPLOAD_IMG_ID,
                             "jcrop_options": jcrop_options,
                             "imagefile": value,
                           }
    return mark_safe(output)


class JcropForm(forms.Form):
  """
  Jcrop form class
  """
  imagefile = forms.Field(widget=JcropWidget(), label="", required=False)
  x1 = forms.DecimalField(widget=forms.HiddenInput)
  y1 = forms.DecimalField(widget=forms.HiddenInput)
  x2 = forms.DecimalField(widget=forms.HiddenInput)
  y2 = forms.DecimalField(widget=forms.HiddenInput)

  def __init__(self, *args, **kwargs):
    """
    overridden init func; check for Jcrop options and remove them
    from kwargs
    """    
    # remove upload image post data (if present); this would make Django form
    # code hick up (since there is no upload image widget in the control)...
    try:
      post_data = args[0]
      if UPLOAD_IMG_ID in post_data:
        del post_data[UPLOAD_IMG_ID]
    except (IndexError):
      # no POST data passed; nothing todo anyway
      pass

    jcrop_options = {}
    if "jcrop_options" in kwargs:
      jcrop_options = kwargs["jcrop_options"]
      del(kwargs["jcrop_options"])

    # call base class __init__
    super(JcropForm, self).__init__(*args, **kwargs)

    # set Jcrop options for our crop widget 
    self.fields["imagefile"].widget.add_jcrop_options(jcrop_options)

  def clean_imagefile(self):
    """
    instantiate PIL image; raise ValidationError if field contains no image
    """
    try:
      self.img = pil.open(settings.MEDIA_ROOT + self.cleaned_data["imagefile"])
    except IOError:
      raise forms.ValidationError("Invalid image file")
    return self.cleaned_data["imagefile"]


  def is_valid(self):
    """
    checks if self._errors is empty; if so, self._errors is set to None and
    full_clean() is called.
    This is necessary since the base class' is_valid() method does
    not populate cleaned_data if _errors is an empty ErrorDict (but not 'None').
    I just failed to work this out by other means...
    """
    if self._errors is not None and len(self._errors) == 0:
      self._errors = None
      self.full_clean()
    return super(JcropForm, self).is_valid()

  def crop (self):
    """
    crop the image to the user supplied coordinates
    """
    x1=self.cleaned_data['x1']
    x2=self.cleaned_data['x2']
    y1=self.cleaned_data['y1']
    y2=self.cleaned_data['y2']
    self.img = self.img.crop((x1, y1, x2, y2))

  def resize (self, dimensions, maintain_ratio=False):
    """
    resize image to dimensions passed in
    """
    if maintain_ratio:
      self.img = self.img.thumbnail(dimensions, pil.ANTIALIAS)
    else:
      self.img = self.img.resize(dimensions, pil.ANTIALIAS)

  def save(self):
    """
    save image...
    """
    self.img.save(settings.MEDIA_ROOT + self.cleaned_data['imagefile'])

  @staticmethod
  def prepare_uploaded_img(files, upload_to, profile, max_display_size=None):
    """
    stores an uploaded image in the proper destination path and 
    optionally resizes it so it can be displayed properly.
    Returns path and filename of the new image (without MEDIA_ROOT).

    'upload_to' must be a function reference as expected by Django's
    FileField object, i.e. a function that expects a profile instance
    and a file name and that returns the final path and name for the
    file. 
    """
    try:
      upload_file = files[UPLOAD_IMG_ID]
    except MultiValueDictKeyError:
      # files dict does not contain new image
      return None

    # copy image data to final file
    fn = upload_to(profile, upload_file.name)
    pfn = settings.MEDIA_ROOT + fn
    destination = open(pfn, 'wb+')
    for chunk in upload_file.chunks():
      destination.write(chunk)
    destination.close()

    if max_display_size:
      # resize image if larger than specified
      im = pil.open(pfn)
      if im.size[0] > max_display_size[0]:
        # image is wider than allowed; resize it
        im = im.resize((max_display_size[0], 
                        im.size[1] * max_display_size[0] / im.size[0]),
                        pil.ANTIALIAS)
      if im.size[1] > max_display_size[1]:
        # image is taller than allowed; resize it
        im = im.resize((im.size[0] * max_display_size[1] / im.size[1], 
                        im.size[1]), pil.ANTIALIAS)
      im.save(pfn)

    return fn