From b1b467ca78585d1b875d41e8374164bbb4952ad6 Mon Sep 17 00:00:00 2001 From: luxagraf Date: Sun, 25 Oct 2015 08:44:23 -0400 Subject: redid photo app to support uploading a zip folder of images and reading photo info from exif metadata --- app/photos/forms.py | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 app/photos/forms.py (limited to 'app/photos/forms.py') diff --git a/app/photos/forms.py b/app/photos/forms.py new file mode 100644 index 0000000..eae0e7e --- /dev/null +++ b/app/photos/forms.py @@ -0,0 +1,174 @@ +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.contrib import messages +from django.contrib.sites.models import Site +from django.conf import settings +from django.utils.encoding import force_text +from django.template.defaultfilters import slugify +from django.core.files.base import ContentFile +from django.contrib.admin import widgets +from django.contrib.gis.geos import Point + +import exiftool +from photos.models import LuxImage, Gallery +from locations.models import Location + +logger = logging.getLogger('photos.forms') + +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 Gallery.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 = Gallery.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'] + ) + 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.image.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: + 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 + + zipper.close() + + if request: + messages.success(request, _('The photos have been uploaded')) -- cgit v1.2.3-70-g09d2