Skip to content

Commit

Permalink
Add JPEGField (#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
codingjoe committed Jul 23, 2019
1 parent 17da737 commit 3249f92
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 11 deletions.
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,23 @@ 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.

Example:

```python
from django.db import models
from stdimage.models import StdImageField
from stdimage import StdImageField, JPEGField


class MyModel(models.Model):
Expand All @@ -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={
Expand All @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion stdimage/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .models import StdImageField # NOQA
from .models import JPEGField, StdImageField # NOQA
69 changes: 66 additions & 3 deletions stdimage/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
15 changes: 14 additions & 1 deletion tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,)
Expand Down Expand Up @@ -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

0 comments on commit 3249f92

Please sign in to comment.