Login

Jcrop Form

Author:
mjthie
Posted:
January 10, 2011
Language:
Python
Version:
1.2
Score:
3 (after 3 ratings)

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

NOTE: Still lacks proper error handling...

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
"""
Django Jcrop form

Author: Markus Thielen, [email protected]

= 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

= 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
        

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

Please login first before commenting.