django-simpleimages¶
This opinionated Django utility will take the image file from one
ImageField
and transform it onto
another field, when the model saves.
Why…?¶
I believe that keeping the implementation of this package as simple as possible. When I say simple, I mean in comparison to other image transformation packages in Django.
The Alternative¶
The most popular apps, such as sorl-thumbnail, generate the transformed images in the request-response cycle, in the template or the views. That was the images are never out of date and are not stored in the database, which makes sense because there isn’t really anything new that should be stored by a scaled down image. And it makes sense that it is present in the template, because it really is a presentation detail. And it is the easiest method, and can be implemented with a few lines of code. For example:
{% thumbnail item.image "100x100" crop="center" as im %}
<img src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}">
{% endthumbnail %}
Performance Problems¶
I ran into performance problems with this approach. Since images are generated in the request-response cycle, caching strategies are essential to minimize database and storage access. I then found django-imagekit, which is much more advanced and allows a great flexibility on every bit of the image generation process. However I still found myself struggling to understand the exact implementation details of how and when the images were generated. This isn’t something I should have been worrying about, apart from the fact that some of my pages were timing out generating hundreds of thumbnails.
My Solution¶
So I decided to write an implementation that anyone could understand.
django-simpleimages
uses the standard
Even though is more verbose, and requires an extra database column,
storing transformed images in their own fields presents several
advantages. It allows
caching of image dimensions, using
Django’s built in solution. It also is easy to understand when the
storage backend is being accessed, because you are simply accessing a
normal ImageField
.
Configuration¶
Add simpleimages
to your INSTALLED_APPS
to use the
management command.
If you want to transform the images using workers, set the
SIMPLEIMAGES_TRANSFORM_CALLER
to a function that will call
the transform function. It defaults to 'simpleimages.callers.default'
,
which transforms images synchronously. See callers
for all
provided image transform callers.
The async docs section has more details on managing image retrieval for async creation.
Requirements¶
- Django 1.5, 1.6, 1.7, 1.8
- Python 2.7, 3.2, 3.3, 3.4, 3.5
Usage¶
Models¶
Here is an example model that will create transformed images on save:
from django.db import models
import simpleimages.transforms
import simpleimages.trackers
class YourModel(models.Model):
image = models.ImageField(
upload_to='images/'
)
thumbnail_image = models.ImageField(
blank=True,
null=True,
editable=False,
upload_to='transformed_images/thumbnails/'
)
large_image = models.ImageField(
blank=True,
null=True,
editable=False,
upload_to='transformed_images/large/'
)
transformed_fields = {
'image': {
'thumbnail_image': simpleimages.transforms.Scale(width=10),
'large_image': simpleimages.transforms.Scale(width=200),
}
}
simpleimages.tracking.track_model(YourModel)
track_model()
is called with the model you want to
track. When that model is saved,
perform_transformation()
uses the transformed_fields
attribute of the model to determine a mapping of source to destination
and transform functions.
See transforms
for all the provided transformations.
Dimension Caching¶
I would recommend using
height_field
and
width_field
to save the image
dimensions. Otherwise (at least with
storages.backends.s3boto.S3BotoStorage
), the file will have
to be retrieved once to get url
and another time to get the dimensions:
import os
from django.db import models
import simpleimages.transforms
import simpleimages.trackers
def image_path_function(subfolder, instance, filename):
return os.path.join(
instance.content_name,
'photos',
subfolder,
filename
)
def original_image_path_function(instance, filename):
return image_path_function('original', instance, filename)
def thumbnail_image_path_function(instance, filename):
return image_path_function('thumbnail', instance, filename)
def large_image_path_function(instance, filename):
return image_path_function('large', instance, filename)
class Photo(models.Model):
image = models.ImageField(
upload_to=original_image_path_function,
max_length=1000,
)
thumbnail_image = models.ImageField(
blank=True,
null=True,
editable=False,
upload_to=thumbnail_image_path_function,
height_field='thumbnail_image_height',
width_field='thumbnail_image_width',
max_length=1000
)
large_image = models.ImageField(
blank=True,
null=True,
editable=False,
upload_to=large_image_path_function,
height_field='large_image_height',
width_field='large_image_width',
max_length=1000
)
# cached dimension fields
thumbnail_image_height = models.PositiveIntegerField(
null=True,
blank=True,
editable=False,
)
thumbnail_image_width = models.PositiveIntegerField(
null=True,
blank=True,
editable=False,
)
large_image_height = models.PositiveIntegerField(
null=True,
blank=True,
editable=False,
)
large_image_width = models.PositiveIntegerField(
null=True,
blank=True,
editable=False,
)
transformed_fields = {
'image': {
'thumbnail_image': simpleimages.transforms.Scale(height=600),
'large_image': simpleimages.transforms.Scale(height=800),
}
}
simpleimages.trackers.track_model(Photo)
Performing Transforms Asynchronously¶
By default all transformations are performed when the model is saved.
If you want to instead perform the transformations asynchronously,
for the obvious performance reasons, you by setting
SIMPLEIMAGES_TRANSFORM_CALLER
. Set this to the dotted
path to any function that will take the transform function as its
first argument and the arguments to call it with as subsequent
arguments and keyword arguments. This format was based around
django-rq. To perform all transforms through django-rq set
SIMPLEIMAGES_TRANSFORM_CALLER='django_rq.enqueue'
.
There is also built in support for celery, just set
SIMPLEIMAGES_TRANSFORM_CALLER='simpleimages.callers.celery'
Then you have to account for the fact that sometimes the transformed images won’t be available in time to render them on the page. If you want to fall back to the source image, if the transformed image isn’t rendered yet, use something like this:
import os
from django.db import models
import simpleimages.transforms
import simpleimages.trackers
def image_path_function(subfolder):
return lambda instance, filename: os.path.join(
instance.content_name,
'photos',
subfolder,
filename
)
def original_image_path_function(instance, filename):
image_path_function('original')(instance, filename)
def thumbnail_image_path_function(instance, filename):
image_path_function('thumbnail')(instance, filename)
def large_image_path_function(instance, filename):
image_path_function('large')(instance, filename)
class Photo(models.Model):
image = models.ImageField(
upload_to=original_image_path_function,
max_length=1000,
)
thumbnail_image = models.ImageField(
blank=True,
null=True,
editable=False,
upload_to=thumbnail_image_path_function,
height_field='thumbnail_image_height',
width_field='thumbnail_image_width',
max_length=1000
)
large_image = models.ImageField(
blank=True,
null=True,
editable=False,
upload_to=large_image_path_function,
height_field='large_image_height',
width_field='large_image_width',
max_length=1000
)
# cached dimension fields
thumbnail_image_height = models.PositiveIntegerField(
null=True,
blank=True,
editable=False,
)
thumbnail_image_width = models.PositiveIntegerField(
null=True,
blank=True,
editable=False,
)
large_image_height = models.PositiveIntegerField(
null=True,
blank=True,
editable=False,
)
large_image_width = models.PositiveIntegerField(
null=True,
blank=True,
editable=False,
)
@property
def safe_thumbnail_image(self):
return self.thumbnail_image or self.image
@property
def safe_large_image(self):
return self.large_image or self.image
transformed_fields = {
'image': {
'thumbnail_image': simpleimages.transforms.Scale(height=600),
'large_image': simpleimages.transforms.Scale(height=800),
}
}
simpleimages.trackers.track_model(Photo)
Then access the transformed images with instance.safe_thumbnail_image
instead.
Management Command¶
Since the images are only transformed on the save of the model, if you
change the transform function, the instances will not be updated until
you resave them. If you want to retransform all the images in a model or
app use management.commands.retransform
Contributing¶
If you find issues or would like to see a feature suppored, head over to the issues section: and report it.
To contribute code in any form, fork the github repository: and clone it locally. Create a new branch for your feature:
git commit -b feature/whatever-you-like
Add proper docstrings to any changed or added code.
Then make sure all the tests past (and write new ones for any new features).
To run the tests:
docker-compose up -d db redis
docker-compose run tests
Compile the documentation and check if it looks right:
docker-compose run tests make docs-html
open docs/build/index.html
Then push the finished feature to github and open a pull request form the branch.
New Release¶
To create a new release:
- Add changes to
docs/source/changelog.rst
, using Releases formatting - Change version in
setup.py
- Change version in
docs/source/conf.py
python setup.py sdist upload
python setup.py bdist_wheel
git tag x.x.x
- Push git tag and commit
- Add release to github tag, with changes and releasion name.
Changelog¶
Next 1.x feature release
- [Feature]: Added support for Django 1.9.
- [Support] #26: Removed lambdas in docs for model.
Next 0.x feature release
- [Feature]: Reimplement progressive and optimize support.
- [Feature]: Added option to not overwrite image.
- [Support]: Increased transform debug logging.
1.3.4 2017.06.06
- [Bug]: Save color profiles with images
1.3.3 2015.12.26
- [Bug]: Fixed Celery task defination.
1.3.2 2015.12.22
- [Bug]: Removed extranious print debugging.
1.3.0 2015.11.08
- [Feature]: Added support for Python 3.5 and Django 1.7, 1.8.
- [Feature]: Added support for Celery.
- [Feature]: Remove support for PQ (it isn’t being maintained).
- [Support]: Changed to use Docker for development.
1.2.0 2014.04.07
1.1.1 2014.01.27
- [Bug]: Fix height/width order. Before they were reversed and broken.
1.1.0 2014.01.14
- [Feature]: Deletion of destination field when no source exists or transformation fails.
- [Feature]: Require Pillow.
- [Support]: Fixed spelling for caller setting.
- [Support]: Display progress for management command.
1.0.5 2013.09.04
- [Bug]: Check if destination field exists before deleting.
1.0.2 2013.08.31
- [Bug]: Fixed adding management command directory
1.0.1 2013.08.31
- [Bug]: Added management directory to packages so that Django finds command
1.0.0 2013.08.23
- [Feature]: Added option to django-rq
- [Support]: Use py.test for testing.
- [Support]: Added Sphinx docs.
0.2.7 2013.06.06
- [Bug]: Save only filename and not whole path for transformed images.
0.2.6 2013.06.06
- [Bug]: Use .count() for management command instead of len()
0.2.5 2013.06.04
- [Bug]: Fixed
retransform
with no fields.
0.2.3 2013.06.04
- [Bug]: Add all packages so that Django finds management command
0.2.2 2013.06.04
- [Bug]: Zip safe on setup.py so Django finds management command.
0.2.0 2013.05.29
- [Feature]: Support uploading of non-image files.
- [Feature]: Save image with higher quality.
- [Feature]: Save image as progressive.
- [Feature] #20: Don’t save image with optimize either, because encoutner error.
- [Support]: Fixed Readme formatting.
- [Support]: Added requirement for at least Django 1.5.
- [Support]: Added instructions to add to
INSTALLED_APPS
. - [Support]: Reasons why to use library added to readme.
0.1.9 2013.05.29
- [Bug]: Don’t save image as progressive, because encounters error.
0.1.8 2013.05.29
- [Bug]: Convert image to JPEG colorspace.
0.1.7 2013.05.29
- [Bug]: Addressed force_update error.
- [Bug]: Transform post save.
0.1.6 2013.05.29
- [Bug]: Moved error handling to transform function.
0.1.5 2013.04.18
- [Bug]: Make sure image exists before trying to delete it.
0.1.0 2013.03.19
- [Feature]: Basic functionality.
simpleimages
Package¶
callers
Module¶
management.commands.retransform
Module¶
-
simpleimages.management.commands.retransform.
parse_model_specifier
(specifier)[source]¶ Parses a string that specifies either a model or a field. The string should look like
app.model.[field]
.>>> print parse_model_specifier('tests.TestModel') (<class 'tests.models.TestModel'>, None) >>> print parse_model_specifier('tests.TestModel.image') (<class 'tests.models.TestModel'>, 'image')
Returns: model and (optionally) field name Return type: tuple of Model
and str or None
trackers
Module¶
-
simpleimages.trackers.
track_model
(model)[source]¶ Perform designated transformations on model, when it saves.
Calls
perform_transformation()
on every model saves usingdjango.db.models.signals.post_save
.It uses the
update_fields
kwarg to tell what fields it should transform.
transforms
Module¶
-
class
simpleimages.transforms.
BasePILTransform
[source]¶ Bases:
object
Base transform object that provides helper methods to transform
django.core.files.images.ImageFile
usingPIL
.Must subclass and override
transform_pil_image()
.-
__call__
(original_django_file)[source]¶ Returns the transformed version of
PIL.Image.Image
Uses
transform_pil_image()
to transform thePIL.Image.Image
.Parameters: original_django_file ( django.core.files.images.ImageFile
) – source fileReturns: transformed file Return type: django.core.files.File
-
django_file_to_pil_image
(django_file)[source]¶ Converts a the file returned by
django.db.models.fields.ImageField
to a PIL image.Parameters: django_file ( django.db.models.fields.files.FieldFile
) – django fileReturn type: PIL.Image.Image
-
pil_image_to_django_file
(pil_image)[source]¶ Gets a PIL image ready to be able to be saved using
django.db.models.fields.files.FieldFile.save()
It converts the mode first to
RGB
orL
, so that it can then save it as aJPEG
. It will save it as a progressiveJPEG
with a quality ofIMAGE_QUALITY
.Parameters: pil_image ( PIL.Image.Image
) – original imageReturns: transformed image Return type: django.core.files.base.ContentFile
-
-
class
simpleimages.transforms.
Scale
(width=None, height=None)[source]¶ Bases:
simpleimages.transforms.BasePILTransform
Scales down an image to max height and/or width. If the original image is smaller than both/either specified dimensions than it will be left unchanged.
-
__init__
(width=None, height=None)[source]¶ Initialize this class with a max height and/or width (in pixels).
Parameters: - height (int or float) – max height of scaled image
- width (int or float) – max width of scaled image
-
transform_pil_image
(pil_image)[source]¶ Uses
PIL.Image.Image.transform()
to scale down the image.Based on this stackoverflow discussions, uses
PIL.Image.ANTIALIAS
-
utils
Module¶
-
simpleimages.utils.
perform_transformation
(instance, field_names_to_transform=None)[source]¶ Transforms a model based on the fields specified in the
transformed_fields
attribute. This should map source image field names to dictionaries mapping destination field name to their transformations. For instance:{ 'image': { 'thumbnail': scale(width=10), } }
If
field_names_to_transform
is None, then it will transform all fields. Otherwise it will only transform from those fields specified infield_names_to_transform
.Parameters: - instance (instance of
django.db.models.Model
) – model instance to perform transformations on - field_names_to_transform (iterable of strings or None) – field names on model to perform transformations on
- instance (instance of
-
simpleimages.utils.
transform_field
(instance, source_field_name, destination_field_name, transformation)[source]¶ Does an image transformation on a instance. It will get the image from the source field attribute of the instnace, then call the transformation function with that instance, and finally save that transformed image into the destination field attribute of the instance.
Note
If the source field is blank or the transformation returns a false value then the destination field image will be deleted, if it exists.
Warning
When the model instance is saved with the new transformed image, it uses the
update_fields
argument forsave()
, to tell the model to only update the destination field and, if set in the destination field, theheight_field
andwidth_field
. This means that if the saving code for the model sets any other fields, in the saving field process, it will not save those fields to the database. This would only happen if you introduce custom logic to the saving process of destination field, like the dimension fields do, that updates another field on that module. In that case, when the model is saved for the transformation, that other field will not be saved to the database.Parameters: - instance (instance of
django.db.models.Model
) – model instance to perform transformations on - source_field_name (string) – field name on model to find source image
- destination_field_name (string) – field name on model save transformed image to
- transformation (function) – function, such as
scale()
, that takes an image files and returns a transformed image
- instance (instance of