mirror of
https://git.linux-kernel.at/oliver/ivatar.git
synced 2025-11-15 04:28:03 +00:00
Feature 'crop photo'
This commit is contained in:
@@ -61,6 +61,9 @@ DEFAULT_FROM_EMAIL = SERVER_EMAIL
|
|||||||
|
|
||||||
MAX_NUM_PHOTOS = 5
|
MAX_NUM_PHOTOS = 5
|
||||||
MAX_PHOTO_SIZE = 10485760 # in bytes
|
MAX_PHOTO_SIZE = 10485760 # in bytes
|
||||||
|
MAX_PIXELS = 7000
|
||||||
|
AVATAR_MAX_SIZE = 512
|
||||||
|
JPEG_QUALITY = 85
|
||||||
|
|
||||||
BOOTSTRAP4 = {
|
BOOTSTRAP4 = {
|
||||||
'include_jquery': False,
|
'include_jquery': False,
|
||||||
|
|||||||
@@ -13,14 +13,19 @@ from urllib.parse import urlsplit, urlunsplit
|
|||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib import messages
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
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 django.utils.translation import ugettext_lazy as _
|
||||||
from openid.association import Association as OIDAssociation
|
from openid.association import Association as OIDAssociation
|
||||||
from openid.store import nonce as oidnonce
|
from openid.store import nonce as oidnonce
|
||||||
from openid.store.interface import OpenIDStore
|
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_LENGTH_EMAIL, logger
|
||||||
|
from ivatar.settings import MAX_PIXELS, AVATAR_MAX_SIZE, JPEG_QUALITY
|
||||||
from .gravatar import get_photo as get_gravatar_photo
|
from .gravatar import get_photo as get_gravatar_photo
|
||||||
|
|
||||||
|
|
||||||
@@ -38,6 +43,17 @@ def file_format(image_type):
|
|||||||
elif image_type == 'GIF':
|
elif image_type == 'GIF':
|
||||||
return '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)
|
logger.info('Unsupported file format: %s' % image_type)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -122,13 +138,81 @@ class Photo(BaseAccountModel):
|
|||||||
# Testing? Ideas anyone?
|
# Testing? Ideas anyone?
|
||||||
except Exception as e: # pylint: disable=unused-variable
|
except Exception as e: # pylint: disable=unused-variable
|
||||||
# For debugging only
|
# For debugging only
|
||||||
# print('Exception caught: %s' % e)
|
print('Exception caught: %s' % e)
|
||||||
return False
|
return False
|
||||||
self.format = file_format(img.format)
|
self.format = file_format(img.format)
|
||||||
if not self.format:
|
if not self.format:
|
||||||
|
print('Format not recognized')
|
||||||
return False
|
return False
|
||||||
return super().save(*args, **kwargs)
|
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):
|
class ConfirmedEmailManager(models.Manager):
|
||||||
'''
|
'''
|
||||||
|
|||||||
55
ivatar/ivataraccount/templates/crop_photo.html
Normal file
55
ivatar/ivataraccount/templates/crop_photo.html
Normal 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();"/>
|
||||||
|
<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 %}
|
||||||
@@ -9,6 +9,7 @@ from . views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
|
|||||||
from . views import ImportPhotoView, RawImageView, DeletePhotoView
|
from . views import ImportPhotoView, RawImageView, DeletePhotoView
|
||||||
from . views import UploadPhotoView, AssignPhotoOpenIDView
|
from . views import UploadPhotoView, AssignPhotoOpenIDView
|
||||||
from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
|
from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
|
||||||
|
from . views import CropPhotoView
|
||||||
from django.contrib.auth.views import login, logout
|
from django.contrib.auth.views import login, logout
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
@@ -71,4 +72,5 @@ urlpatterns = [
|
|||||||
'delete_photo/(?P<pk>\d+)',
|
'delete_photo/(?P<pk>\d+)',
|
||||||
DeletePhotoView.as_view(), name='delete_photo'),
|
DeletePhotoView.as_view(), name='delete_photo'),
|
||||||
url('raw_image/(?P<pk>\d+)', RawImageView.as_view(), name='raw_image'),
|
url('raw_image/(?P<pk>\d+)', RawImageView.as_view(), name='raw_image'),
|
||||||
|
url('crop_photo/(?P<pk>\d+)', CropPhotoView.as_view(), name='crop_photo'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -355,6 +355,8 @@ class UploadPhotoView(SuccessMessageMixin, FormView):
|
|||||||
messages.error(self.request, _('Invalid Format'))
|
messages.error(self.request, _('Invalid Format'))
|
||||||
return HttpResponseRedirect(reverse_lazy('profile'))
|
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)
|
return super().form_valid(form, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@@ -513,3 +515,46 @@ class ConfirmOpenIDView(View): # pragma: no cover
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
return self.do_request(request.POST, *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)
|
||||||
|
|||||||
Reference in New Issue
Block a user