diff --git a/exampleproject/blog/models.py b/exampleproject/blog/models.py index c71a73b..dcd875e 100644 --- a/exampleproject/blog/models.py +++ b/exampleproject/blog/models.py @@ -8,21 +8,25 @@ class BlogPost(models.Model): title = models.CharField(max_length=100) # an image with thumbnails - artwork = ImageWithThumbnailsField(max_length=255, - upload_to='artwork/', - thumbnails=(('homepage_image', CropRenderer(300, 150)), - ('pagination_image', CropRenderer(150, 75))), - help_text='Source artwork. Thumbnails automatically generated from this field.') + artwork = ImageWithThumbnailsField( + max_length=255, + upload_to='artwork/', + blank=True, + thumbnails=(('homepage_image', CropRenderer(300, 150)), + ('pagination_image', CropRenderer(150, 75))), + help_text='Source artwork. Thumbnails automatically generated ' + 'from this field.') # an override field, capable of rolling up a path - # when it has no value. useful for overriding + # when it has no value. useful for overriding # auto-generated thumbnails. the fallback path # should point to an ImageFieldFile of some sort. # # under the hood, this is just an ImageField - homepage_image = ImageFallbackField(fallback_path='artwork.thumbnails.homepage_image', - upload_to='artwork/', - help_text='Optional override for "homepage_image" thumbnail.') + homepage_image = ImageFallbackField( + fallback_path='artwork.thumbnails.homepage_image', + upload_to='artwork/', + help_text='Optional override for "homepage_image" thumbnail.') def __unicode__(self): return self.title diff --git a/undermythumb/fields.py b/undermythumb/fields.py index 353d4a3..5f7a735 100644 --- a/undermythumb/fields.py +++ b/undermythumb/fields.py @@ -3,7 +3,6 @@ from django.db.models.fields.files import (ImageField, ImageFieldFile, ImageFileDescriptor) -from django.utils.encoding import force_unicode from undermythumb.files import (ThumbnailFieldFile, ImageWithThumbnailsFieldFile) @@ -98,10 +97,33 @@ def __init__(self, thumbnails=None, fallback_path=None, *args, **kwargs): self.thumbnails = thumbnails or [] self.fallback_path = fallback_path - def get_thumbnail_filename(self, instance, original, key, ext): - # return filename - base, _ext = os.path.splitext(force_unicode(original)) - return '%s-%s%s' % (base, key, ext) + def get_thumbnail_filename(self, instance, original_file, + thumbnail_name, ext): + """Generates a predictable thumbnail filename. + + Thumbnail generation follows this template: :: + + {thumbnail_name}.{source file hash}.{ext} + + Place no expensive calculations here -- this runs any + time a thumbnail field is accessed. + + :param instance: Model instance containing the field + :param original_file: Uploaded image file + :param thumbnail_name: Model instance field name + :param ext: File extension *with* '.' separator. + """ + + path = os.path.dirname(original_file.name) + hash_value, _ = os.path.splitext(os.path.basename(original_file.name)) + + filename = '%(thumbnail)s.%(hash)s%(ext)s' % { + 'path': path, + 'thumbnail': thumbnail_name, + 'hash': hash_value, + 'ext': ext} + + return os.path.join(path, filename) def south_field_triple(self): """Return a description of this field for South. diff --git a/undermythumb/files.py b/undermythumb/files.py index 0dc14d8..b5e3fa9 100644 --- a/undermythumb/files.py +++ b/undermythumb/files.py @@ -1,3 +1,6 @@ +from hashlib import sha1 +import os + from django.db.models.fields.files import ImageFieldFile @@ -24,20 +27,20 @@ def _populate(self): key = attname ext = '.%s' % renderer.format - name = self.field.get_thumbnail_filename( - instance=self.instance, - original=self.file, - key=key, - ext=ext) + name = self.field.get_thumbnail_filename( + instance=self.instance, + original_file=self.file, + thumbnail_name=key, + ext=ext) - thumbnail = ThumbnailFieldFile( - attname, - renderer, - self.instance, - self.field, - name) + thumbnail = ThumbnailFieldFile( + attname, + renderer, + self.instance, + self.field, + name) - self._cache[attname] = thumbnail + self._cache[attname] = thumbnail def clear_cache(self): self._cache = {} @@ -74,13 +77,20 @@ def __init__(self, *args, **kwargs): self.thumbnails = ThumbnailSet(self) def save(self, name, content, save=True): - """Save the original image, and its thumbnails. - """ + # set file name to first 8 chars of hash of contents + _, ext = os.path.splitext(name) + file_hash = sha1(content.read()).hexdigest()[:8] + name = file_hash + ext + + # save source file super(ImageWithThumbnailsFieldFile, self).save(name, content, save) self.thumbnails.clear_cache() - # iterate over thumbnail for thumbnail in self.thumbnails: rendered = thumbnail.renderer.generate(content) self.field.storage.save(thumbnail.name, rendered) + setattr(self.instance, self.field.attname, thumbnail.name) + + if save: + self.instance.save() diff --git a/undermythumb/thumbnails.py b/undermythumb/thumbnails.py deleted file mode 100644 index 62e35b2..0000000 --- a/undermythumb/thumbnails.py +++ /dev/null @@ -1,51 +0,0 @@ -from undermythumb.files import ThumbnailFieldFile - - -class ThumbnailSet(object): - - def __init__(self, field_file): - self.file = field_file - self.field = self.file.field - self.instance = self.file.instance - - self._cache = {} - self._populate() - - def _populate(self): - if not self._cache and self.file.name and self.instance.id: - for options in self.field.thumbnails: - try: - attname, renderer, key = options - except ValueError: - attname, renderer = options - key = attname - ext = '.%s' % renderer.format - - name = self.field.get_thumbnail_filename( - instance=self.instance, - original=self.file, - key=key, - ext=ext) - - thumbnail = ThumbnailFieldFile( - attname, - renderer, - self.instance, - self.field, - name) - - self._cache[attname] = thumbnail - - def clear_cache(self): - self._cache = {} - - def __getattr__(self, name): - try: - return self._cache[name] - except KeyError: - return None - - def __iter__(self): - self._populate() - for attname, value in self._cache.iteritems(): - yield value