From 3249f92e522aceae771f1ebcd90a76bf35a949a5 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Tue, 23 Jul 2019 17:44:17 +0200 Subject: [PATCH] Add JPEGField (#211) --- README.md | 20 +++++++++---- stdimage/__init__.py | 2 +- stdimage/models.py | 69 ++++++++++++++++++++++++++++++++++++++++++-- tests/models.py | 15 +++++++++- tests/test_models.py | 9 ++++++ 5 files changed, 104 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0a042ed..7ee097f 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,15 @@ and add `'stdimage'` to `INSTALLED_APP`s in your settings.py, that's it! ## Usage - -``StdImageField`` works just like Django's own +`StdImageField` works just like Django's own [ImageField](https://docs.djangoproject.com/en/dev/ref/models/fields/#imagefield) except that you can specify different sized variations. +The `JPEGField` works similar to the `StdImageField` but all size variations are +converted to JPEGs, no matter what type the original file is. + ### Variations + Variations are specified within a dictionary. The key will be the attribute referencing the resized image. A variation can be defined both as a tuple or a dictionary. @@ -43,7 +46,7 @@ Example: ```python from django.db import models -from stdimage.models import StdImageField +from stdimage import StdImageField, JPEGField class MyModel(models.Model): @@ -56,6 +59,12 @@ class MyModel(models.Model): # is the same as dictionary-style call image = StdImageField(upload_to='path/to/img', variations={'thumbnail': (100, 75)}) + + # variations are converted to JPEGs + jpeg = JPEGField( + upload_to='path/to/img', + variations={'full': (float('inf'), float('inf')), 'thumbnail': (100, 75)}, + ) # creates a thumbnail resized to 100x100 croping if necessary image = StdImageField(upload_to='path/to/img', variations={ @@ -67,11 +76,10 @@ class MyModel(models.Model): 'large': (600, 400), 'thumbnail': (100, 100, True), 'medium': (300, 200), - delete_orphans=True, - }) + }, delete_orphans=True) ``` - For using generated variations in templates use `myimagefield.variation_name`. +For using generated variations in templates use `myimagefield.variation_name`. Example: diff --git a/stdimage/__init__.py b/stdimage/__init__.py index 4be4a69..b762138 100644 --- a/stdimage/__init__.py +++ b/stdimage/__init__.py @@ -1 +1 @@ -from .models import StdImageField # NOQA +from .models import JPEGField, StdImageField # NOQA diff --git a/stdimage/models.py b/stdimage/models.py index 055c44b..1b54ffe 100644 --- a/stdimage/models.py +++ b/stdimage/models.py @@ -18,7 +18,7 @@ class StdImageFileDescriptor(ImageFileDescriptor): """The variation property of the field is accessible in instance cases.""" def __set__(self, instance, value): - super(StdImageFileDescriptor, self).__set__(instance, value) + super().__set__(instance, value) self.field.set_variations(instance) @@ -170,7 +170,7 @@ class StdImageField(ImageField): 'width': float('inf'), 'height': float('inf'), 'crop': False, - 'resample': Image.ANTIALIAS + 'resample': Image.ANTIALIAS, } def __init__(self, verbose_name=None, name=None, variations=None, @@ -236,8 +236,9 @@ def __init__(self, verbose_name=None, name=None, variations=None, def add_variation(self, name, params): variation = self.def_variation.copy() + variation["kwargs"] = {} if isinstance(params, (list, tuple)): - variation.update(dict(zip(("width", "height", "crop"), params))) + variation.update(dict(zip(("width", "height", "crop", "kwargs"), params))) else: variation.update(params) variation["name"] = name @@ -287,3 +288,65 @@ def save_form_data(self, instance, data): if file and file._committed and file != data: file.delete(save=False) super().save_form_data(instance, data) + + +class JPEGFieldFile(StdImageFieldFile): + + @classmethod + def get_variation_name(cls, file_name, variation_name): + path = super().get_variation_name(file_name, variation_name) + path, ext = os.path.splitext(path) + return '%s.jpeg' % path + + @classmethod + def process_variation(cls, variation, image): + """Process variation before actual saving.""" + save_kargs = {} + file_format = 'JPEG' + save_kargs['format'] = file_format + + resample = variation['resample'] + + factor = 1 + while image.size[0] / factor \ + > 2 * variation['width'] \ + and image.size[1] * 2 / factor \ + > 2 * variation['height']: + factor *= 2 + if factor > 1: + image.thumbnail( + (int(image.size[0] / factor), + int(image.size[1] / factor)), + resample=resample + ) + + size = variation['width'], variation['height'] + size = tuple(int(i) if i != float('inf') else i + for i in size) + + # http://stackoverflow.com/a/21669827 + image = image.convert('RGB') + save_kargs['optimize'] = True + save_kargs['quality'] = 'web_high' + if size[0] * size[1] > 10000: # roughly <10kb + save_kargs['progressive'] = True + + if variation['crop']: + image = ImageOps.fit( + image, + size, + method=resample + ) + else: + image.thumbnail( + size, + resample=resample + ) + + save_kargs.update(variation['kwargs']) + + return image, save_kargs + + +class JPEGField(StdImageField): + attr_class = JPEGFieldFile diff --git a/tests/models.py b/tests/models.py index d2077ff..8f861e0 100644 --- a/tests/models.py +++ b/tests/models.py @@ -5,7 +5,7 @@ from django.db import models from PIL import Image -from stdimage import StdImageField +from stdimage import JPEGField, StdImageField from stdimage.models import StdImageFieldFile from stdimage.utils import render_variations from stdimage.validators import MaxSizeValidator, MinSizeValidator @@ -59,6 +59,19 @@ class ThumbnailModel(models.Model): ) +class JPEGModel(models.Model): + """creates a thumbnail resized to maximum size to fit a 100x75 area""" + image = JPEGField( + upload_to=upload_to, + blank=True, + variations={ + 'full': (float('inf'), float('inf')), + 'thumbnail': (100, 75, True), + }, + delete_orphans=True, + ) + + class MaxSizeModel(models.Model): image = StdImageField( upload_to=upload_to, diff --git a/tests/test_models.py b/tests/test_models.py index 5e6a5b8..a8b9006 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -7,6 +7,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from PIL import Image +from . import models from .models import (AdminDeleteModel, CustomRenderVariationsModel, ResizeCropModel, ResizeModel, SimpleModel, ThumbnailModel, ThumbnailWithoutDirectoryModel, UtilVariationsModel,) @@ -222,3 +223,11 @@ def test_min_size_validator(self, admin_client): 'image': self.fixtures['100.gif'], }) assert not os.path.exists(os.path.join(IMG_DIR, '100.gif')) + + +class TestJPEGField(TestStdImage): + def test_convert(self, db): + obj = models.JPEGModel.objects.create(image=self.fixtures['100.gif']) + assert obj.image.thumbnail.path.endswith('img/100.thumbnail.jpeg') + assert obj.image.full.width == 100 + assert obj.image.full.height == 100