import zipfile from zipfile import BadZipFile import logging import datetime import time import os from io import BytesIO from fractions import Fraction try: import Image except ImportError: from PIL import Image from django import forms from django.utils.translation import ugettext_lazy as _ from django.utils.safestring import mark_safe from django.contrib import messages from django.core.files.base import ContentFile from django.contrib.admin import widgets from django.contrib.gis.geos import Point from django.conf import settings import exiftool from photos.models import LuxImage, LuxGallery from locations.models import Location from .utils import resize_image logger = logging.getLogger('photos.forms') class GalleryForm(forms.ModelForm): class Meta: model = LuxGallery fields = '__all__' widgets = { 'images': forms.SelectMultiple, } def __init__(self, *args, **kwargs): super(GalleryForm, self).__init__(*args, **kwargs) images = LuxImage.objects.all()[:100] items = [] for image in images: items.append( (image.pk, mark_safe('%sqq%s' % (image.title, image.get_image_name()))) ) self.fields['images'].choices = items self.fields['images'].allow_tags = True class UploadZipForm(forms.Form): """ Handles the uploading of a gallery of photos packed in a .zip file Creates Gallery object, adds photos with all metadata that's available """ zip_file = forms.FileField() title = forms.CharField(label=_('Gallery Title'), max_length=250) slug = forms.SlugField(label=_('Gallery Slug')) desc = forms.CharField(label=_('Gallery Caption'), widget=forms.Textarea, required=False) date = forms.SplitDateTimeField(label=_('Date'), widget=widgets.AdminSplitDateTime) is_public = forms.BooleanField(label=_('Is public'), initial=True, required=False, help_text=_('Show on site')) def clean_zip_file(self): """Open the zip file a first time, to check that it is a valid zip archive. We'll open it again in a moment, so we have some duplication, but let's focus on keeping the code easier to read! """ zip_file = self.cleaned_data['zip_file'] try: zip = zipfile.ZipFile(zip_file) except BadZipFile as e: raise forms.ValidationError(str(e)) bad_file = zip.testzip() if bad_file: zip.close() raise forms.ValidationError('"%s" in the .zip archive is corrupt.' % bad_file) zip.close() # Close file in all cases. return zip_file def clean_title(self): title = self.cleaned_data['title'] if title and LuxGallery.objects.filter(title=title).exists(): raise forms.ValidationError(_('A gallery with that title already exists.')) return title def clean(self): cleaned_data = super(UploadZipForm, self).clean() if not self['title'].errors: # If there's already an error in the title, no need to add another # error related to the same field. if not cleaned_data.get('title', None) and not cleaned_data['gallery']: raise forms.ValidationError( _('Select an existing gallery, or enter a title for a new gallery.')) return cleaned_data def save(self, request=None, zip_file=None): if not zip_file: zip_file = self.cleaned_data['zip_file'] gallery, created = LuxGallery.objects.get_or_create( title=self.cleaned_data['title'], description=self.cleaned_data['desc'], slug=self.cleaned_data['slug'], pub_date=self.cleaned_data['date'], is_public=self.cleaned_data['is_public'] ) zipper = zipfile.ZipFile(zip_file) count = 1 for filename in sorted(zipper.namelist()): f, file_extension = os.path.splitext(filename) logger.debug('Reading file "{0}".'.format(filename)) if filename.startswith('__') or filename.startswith('.'): logger.debug('Ignoring file "{0}".'.format(filename)) continue if os.path.dirname(filename): logger.warning('Ignoring file "{0}" as it is in a subfolder; all images should be in the top ' 'folder of the zip.'.format(filename)) if request: messages.warning(request, _('Ignoring file "{filename}" as it is in a subfolder').format(filename=filename), fail_silently=True) continue data = zipper.read(filename) if not len(data): logger.debug('File "{0}" is empty.'.format(filename)) continue # Basic check that we have a valid image. try: file = BytesIO(data) opened = Image.open(file) opened.verify() except Exception: # Pillow (or PIL) doesn't recognize it as an image. # If a "bad" file is found we just skip it. # But we do flag this both in the logs and to the user. logger.error('Could not process file "{0}" in the .zip archive.'.format( filename)) if request: messages.warning(request, _('Could not process file "{0}" in the .zip archive.').format( filename), fail_silently=True) continue image = LuxImage( pub_date=datetime.datetime.now() ) contentfile = ContentFile(data) image.image.save(filename, contentfile) image.save() gallery.images.add(image) with exiftool.ExifTool() as et: meta = et.get_metadata(image.image.path) et.terminate() image.exif_raw = meta try: image.title = meta["EXIF:ImageDescription"] except: pass try: image.caption = meta["EXIF:UserComment"] except: pass try: image.exif_lens = meta["MakerNotes:LensType"] except: try: image.exif_lens = meta["XMP:Lens"] except: pass try: image.point = Point(meta["XMP:GPSLongitude"], meta["XMP:GPSLatitude"], srid=4326) try: image.location = Location.objects.filter(geometry__contains=image.point).get() except Location.DoesNotExist: pass except KeyError: pass image.exif_aperture = meta["EXIF:FNumber"] image.exif_make = meta["EXIF:Make"] image.exif_model = meta["EXIF:Model"] image.exif_exposure = str(Fraction(float(meta["EXIF:ExposureTime"])).limit_denominator()) image.exif_iso = meta["EXIF:ISO"] image.exif_focal_length = meta["EXIF:FocalLength"] fmt_date = time.strptime(meta["EXIF:DateTimeOriginal"], "%Y:%m:%d %H:%M:%S") image.exif_date = time.strftime("%Y-%m-%d %H:%M:%S", fmt_date) image.height = meta["File:ImageHeight"] image.width = meta["File:ImageWidth"] image.save() count += 1 img = Image.open(image.image.path) base_path = "%s/galleries/" % settings.IMAGES_ROOT if img.size[0] > img.size[1]: resize_image(img, 2280, None, 65, base_path+'large/', image.get_image_name()) resize_image(img, 1140, None, 72, base_path+'medium/', image.get_image_name()) resize_image(img, 720, None, 68, base_path+'small/', image.get_image_name()) if img.size[1] > img.size[0]: resize_image(img, None, 1600, 65, base_path+'large/', image.get_image_name()) resize_image(img, None, 800, 72, base_path+'medium/', image.get_image_name()) resize_image(img, None, 460, 60, base_path+'small/', image.get_image_name()) resize_image(img, 160, None, 68, base_path+'thumb/', image.get_image_name()) zipper.close() if request: messages.success(request, _('The photos have been uploaded'))