Feature 'crop photo'

This commit is contained in:
Oliver Falk
2018-06-14 14:01:01 +02:00
parent e29c17ff0e
commit 6a4fce0177
5 changed files with 190 additions and 1 deletions

View File

@@ -61,6 +61,9 @@ DEFAULT_FROM_EMAIL = SERVER_EMAIL
MAX_NUM_PHOTOS = 5
MAX_PHOTO_SIZE = 10485760 # in bytes
MAX_PIXELS = 7000
AVATAR_MAX_SIZE = 512
JPEG_QUALITY = 85
BOOTSTRAP4 = {
'include_jquery': False,

View File

@@ -13,14 +13,19 @@ from urllib.parse import urlsplit, urlunsplit
from PIL import Image
from django.contrib.auth.models import User
from django.contrib import messages
from django.db import models
from django.utils import timezone
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from openid.association import Association as OIDAssociation
from openid.store import nonce as oidnonce
from openid.store.interface import OpenIDStore
from ipware import get_client_ip
from ivatar.settings import MAX_LENGTH_EMAIL, logger
from ivatar.settings import MAX_PIXELS, AVATAR_MAX_SIZE, JPEG_QUALITY
from .gravatar import get_photo as get_gravatar_photo
@@ -38,6 +43,17 @@ def file_format(image_type):
elif image_type == 'GIF':
return 'gif'
def pil_format(image_type):
'''
Helper method returning the 'encoder name' for PIL
'''
if image_type == 'jpg':
return 'JPEG'
elif image_type == 'png':
return 'PNG'
elif image_type == 'gif':
return 'GIF'
logger.info('Unsupported file format: %s' % image_type)
return None
@@ -122,13 +138,81 @@ class Photo(BaseAccountModel):
# Testing? Ideas anyone?
except Exception as e: # pylint: disable=unused-variable
# For debugging only
# print('Exception caught: %s' % e)
print('Exception caught: %s' % e)
return False
self.format = file_format(img.format)
if not self.format:
print('Format not recognized')
return False
return super().save(*args, **kwargs)
def perform_crop(self, request, dimensions, email, openid):
if request.user.photo_set.count() == 1:
# This is the first photo, assign to all confirmed addresses
for email in request.user.confirmedemail_set.all():
email.photo = self
email.save()
for openid in request.user.confirmedopenid_set.all():
openid.photo = self
openid.save()
if email:
# Explicitely asked
email.photo = self
email.save()
if openid:
# Explicitely asked
openid.photo = self
openid.save()
# Do the real work cropping
img = Image.open(BytesIO(self.data))
# This should be anyway checked during save...
a, b = img.size
if a > MAX_PIXELS or b > MAX_PIXELS:
messages.error(request, _('Image dimensions are too big(max: %s x %s' % (MAX_PIXELS, MAX_PIXELS)))
return HttpResponseRedirect(reverse_lazy('profile'))
w = dimensions['w']
h = dimensions['h']
x = dimensions['x']
y = dimensions['y']
if w == 0 and h == 0:
w, h = a, b
i = min(w, h)
w, h = i, i
elif w < 0 or (x + w) > a or h < 0 or (y + h) > b:
messages.error(request, _('Crop outside of original image bounding box'))
return HttpResponseRedirect(reverse_lazy('profile'))
cropped = img.crop((x, y, x + w, y + h))
# cropped.load()
# Resize the image only if it's larger than the specified max width.
cropped_w, cropped_h = cropped.size
max_w = AVATAR_MAX_SIZE
if cropped_w > max_w or cropped_h > max_w:
cropped = cropped.resize((max_w, max_w), Image.ANTIALIAS)
data = BytesIO()
cropped.save(data, pil_format(self.format), quality=JPEG_QUALITY)
data.seek(0)
# Create new photo?
# photo = Photo()
# photo.user = request.user
# photo.ip_address = get_client_ip(request)
# photo.data = data.read()
# photo.save()
# Overwrite the existing image
self.data = data.read()
self.save()
return HttpResponseRedirect(reverse_lazy('profile'))
class ConfirmedEmailManager(models.Manager):
'''

View File

@@ -0,0 +1,55 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block title %}{% trans 'Crop photo' %} - ivatar{% endblock title %}
{% block header %}<link rel="prefetch" href="{% static 'css/jcrop.css' %}">{% endblock header %}
{% block content %}
<h1>{% trans 'Crop photo' %}</h1>
<p>{% trans 'Draw a square around the portion of the image you want to use:' %}</p>
<form action="{% url 'crop_photo' photo.pk %}" method="post">{% csrf_token %}
{% if email %}<input type="hidden" name="email" value="{{email}}">{% endif %}
{% if openid %}<input type="hidden" name="openid" value="{{openid}}">{% endif %}
<p class="aligned wide"><img src='{% url 'raw_image' photo.pk %}' id='cropbox' /></p>
<input type='hidden' id='x' name='x' value='0'/>
<input type='hidden' id='y' name='y' value='0'/>
<input type='hidden' id='w' name='w' value='0'/>
<input type='hidden' id='h' name='h' value='0'/>
<p><input type="submit" value="{% trans 'Crop' %}" onsubmit="return checkCoords();"/>
&nbsp;<a id="cancel-link" href="{% url 'profile' %}">{% trans 'Cancel' %}</a></p>
</form>
{% endblock content %}
{% block bootstrap4_after_content %}
{{ block.super }}
<script src="{% static '/js/jcrop.js' %}"></script>
<script type="text/javascript">
function updateCoords(c) {
$('#x').val(c.x);
$('#y').val(c.y);
$('#w').val(c.w);
$('#h').val(c.h);
};
function checkCoords() {
if (parseInt($('#w').val())) return true;
alert('Please select a crop region then press submit.');
return false;
};
</script>
<script type="text/javascript">
jQuery(function($){
$('#cropbox').Jcrop({
onSelect: updateCoords
});
});
</script>
{% endblock bootstrap4_after_content %}

View File

@@ -9,6 +9,7 @@ from . views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
from . views import ImportPhotoView, RawImageView, DeletePhotoView
from . views import UploadPhotoView, AssignPhotoOpenIDView
from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
from . views import CropPhotoView
from django.contrib.auth.views import login, logout
from django.urls import reverse_lazy
@@ -71,4 +72,5 @@ urlpatterns = [
'delete_photo/(?P<pk>\d+)',
DeletePhotoView.as_view(), name='delete_photo'),
url('raw_image/(?P<pk>\d+)', RawImageView.as_view(), name='raw_image'),
url('crop_photo/(?P<pk>\d+)', CropPhotoView.as_view(), name='crop_photo'),
]

View File

@@ -355,6 +355,8 @@ class UploadPhotoView(SuccessMessageMixin, FormView):
messages.error(self.request, _('Invalid Format'))
return HttpResponseRedirect(reverse_lazy('profile'))
# Override success URL -> Redirect to crop page.
self.success_url = reverse_lazy('crop_photo', args=[photo.pk])
return super().form_valid(form, *args, **kwargs)
@@ -513,3 +515,46 @@ class ConfirmOpenIDView(View): # pragma: no cover
def post(self, request, *args, **kwargs):
return self.do_request(request.POST, *args, **kwargs)
@method_decorator(login_required, name='dispatch')
class CropPhotoView(TemplateView):
'''
View class for cropping photos
'''
template_name = 'crop_photo.html'
success_url = reverse_lazy('profile')
model = Photo
def get(self, request, *args, **kwargs):
photo = self.model.objects.get(pk=kwargs['pk'], user=request.user)
email = request.GET.get('email')
openid = request.GET.get('openid')
return render(self.request, self.template_name, {
'photo': photo,
'email': email,
'openid': openid,
})
def post(self, request, *args, **kwargs):
photo = self.model.objects.get(pk=kwargs['pk'], user=request.user)
dimensions = {
'x': int(request.POST['x']),
'y': int(request.POST['y']),
'w': int(request.POST['w']),
'h': int(request.POST['h'])
}
email = openid = None
if 'email' in request.POST:
try:
email = ConfirmedEmail.objects.get(email=request.POST['email'])
except:
pass # Ignore automatic assignment
if 'openid' in request.POST:
try:
openid = ConfirmedOpenId.objects.get(openid=request.POST['openid'])
except:
pass # Ignore automatic assignment
return photo.perform_crop(request, dimensions, email, openid)