Writing Custom Sizers and Filters¶
It’s quick and easy to create new Sizers and Filters for use on your
project’s VersatileImageField
fields or modify already-registered
Sizers and Filters.
Both Sizers and Filters subclass from
versatileimagefield.datastructures.base.ProcessedImage
which
provides a preprocessing API as well as all
the business logic necessary to retrieve and save images.
The ‘meat’ of each Sizer & Filter – what actually modifies the
original image – resides within the process_image
method which
all subclasses must define (not doing so will raise a
NotImplementedError
). Sizers and Filters expect slightly different
keyword arguments (Sizers required width
and height
, for
example) see below for specifics:
Writing a Custom Sizer¶
All Sizers should subclass
versatileimagefield.datastructures.sizedimage.SizedImage
and, at a
minimum, MUST do two things:
- Define either the
filename_key
attribute or override theget_filename_key()
method which is necessary for creating unique-to-Sizer-and-size-specified filenames. If neither of the aforementioned is done aNotImplementedError
exception will be raised. - Define a
process_image
method that accepts the following arguments:image
: a PIL Image instanceimage_format
: A valid image mime type (e.g. ‘image/jpeg’). This is provided by thecreate_resized_image
method (which callsprocess_image
).save_kwargs
: Adict
of any keyword arguments needed by PIL’sImage.save
method (initially provided by the pre-processing API).width
: An integer representing the width specified by the user in the size key.height
: An integer representing the height specified by the user in the size key.
For an example, let’s take a look at the thumbnail
Sizer (versatileimagefield.versatileimagefield.ThumbnailImage
):
import StringIO
from PIL import Image
from .datastructures import SizedImage
class ThumbnailImage(SizedImage):
"""
Sizes an image down to fit within a bounding box
See the `process_image()` method for more information
"""
filename_key = 'thumbnail'
def process_image(self, image, image_format, save_kwargs,
width, height):
"""
Returns a StringIO instance of `image` that will fit
within a bounding box as specified by `width`x`height`
"""
imagefile = StringIO.StringIO()
image.thumbnail(
(width, height),
Image.ANTIALIAS
)
image.save(
imagefile,
**save_kwargs
)
return imagefile
Important
process_image
should always return a StringIO instance. See What process_image should return for more information.
Writing a Custom Filter¶
All Filters should subclass
versatileimagefield.datastructures.filteredimage.FilteredImage
and
only need to define a process_filter
method with following
arguments:
image
: a PIL Image instanceimage_format
: A valid image mime type (e.g. ‘image/jpeg’). This is provided by thecreate_resized_image()
method (which callsprocess_image
).save_kwargs
: Adict
of any keyword arguments needed by PIL’sImage.save
method (initially provided by the pre-processing API).
For an example, let’s take a look at the invert
Filter
(versatileimagefield.versatileimagefield.InvertImage
):
import StringIO
from PIL import ImageOps
from .datastructures import FilteredImage
class InvertImage(FilteredImage):
"""
Inverts the colors of an image.
See the `process_image()` for more specifics
"""
def process_image(self, image, image_format, save_kwargs={}):
"""
Returns a StringIO instance of `image` with inverted colors
"""
imagefile = StringIO.StringIO()
inv_image = ImageOps.invert(image)
inv_image.save(
imagefile,
**save_kwargs
)
return imagefile
Important
process_image
should always return a StringIO
instance. See What process_image should return for more information.
What process_image
should return¶
Any process_image
method you write should always return a
StringIO
instance comprised of raw image data. The actual image file
will be written to your field’s storage class via the save_image
method. Note how save_kwargs
is passed into PIL’s Image.save
method in the examples above, this ensures PIL knows how to write this
data (based on mime type or any other per-filetype specific options
provided by the preprocessing API).
The Pre-processing API¶
Both Sizers and Filters have access to a pre-processing API that provides hooks for doing any per-mime-type processing. This allows your Sizers and Filters to do one thing for JPEGs and another for GIFs, for instance. One example of this is in how Sizers ‘know’ how to preserve transparency for GIFs or save JPEGs as RGB (at the user-defined quality):
# versatileimagefield/datastructures/sizedimage.py
class SizedImage(ProcessedImage, dict):
"<a bunch of ommited code here>"
def preprocess_GIF(self, image, **kwargs):
"""
Receives a PIL Image instance of a GIF and returns 2-tuple:
* [0]: Original Image instance (passed to `image`)
* [1]: Dict with a transparency key (to GIF transparency layer)
"""
return (image, {'transparency': image.info['transparency']})
def preprocess_JPEG(self, image, **kwargs):
"""
Receives a PIL Image instance of a JPEG and returns 2-tuple:
* [0]: Image instance, converted to RGB
* [1]: Dict with a quality key (mapped to the value of `QUAL` as
defined by the `VERSATILEIMAGEFIELD_JPEG_RESIZE_QUALITY`
setting)
"""
if image.mode != 'RGB':
image = image.convert('RGB')
return (image, {'quality': QUAL})
All pre-processors should accept one required argument image
(A PIL
Image instance) and **kwargs
(for easy extension by subclasses) and
return a 2-tuple of the image and a dict of any additional keyword
arguments to pass along to PIL’s Image.save
method.
Pre-processor Naming Convention¶
In order for preprocessor methods to run, they need to be named
correctly via this simple naming convention: preprocess_FILETYPE
.
Here’s a list of all currently-supported file types:
- BMP
- DCX
- EPS
- GIF
- JPEG
- PCD
- PCX
- PNG
- PPM
- PSD
- TIFF
- XBM
- XPM
So, if you’d want to write a PNG-specific preprocessor, your Sizer or
Filter would need to define a method named preprocess_PNG
.
Note
I’ve only tested VersatileImageField
with PNG, GIF and JPEG
files; the list above is what PIL supports, for more information
about per filetype support in PIL visit
here.
Registering Sizers and Filters¶
Registering Sizers and Filters is easy and straight-forward; if you’ve
ever registered a model with django’s admin
you’ll feel right at
home.
django-versatileimagefield
finds Sizers & Filters within modules named
versatileimagefield
– (i.e. versatileimagefield.py
)
that are available at the ‘top level’ of each app on INSTALLED_APPS
.
Here’s an example:
somedjangoapp/
__init__.py
models.py # Models
admin.py # Admin config
versatilimagefield.py # Custom Sizers and Filters here
After defining your Sizers and Filters you’ll need to register them with
the versatileimagefield_registry
. Here’s how the ThumbnailSizer
is registered (see the highlighted lines in the following code block for the relevant bits):
# versatileimagefield/versatileimagefield.py
import StringIO
from PIL import Image
from .datastructures import SizedImage
from .registry import versatileimagefield_registry
class ThumbnailImage(SizedImage):
"""
Sizes an image down to fit within a bounding box
See the `process_image()` method for more information
"""
filename_key = 'thumbnail'
def process_image(self, image, image_format, save_kwargs,
width, height):
"""
Returns a StringIO instance of `image` that will fit
within a bounding box as specified by `width`x`height`
"""
imagefile = StringIO.StringIO()
image.thumbnail(
(width, height),
Image.ANTIALIAS
)
image.save(
imagefile,
**save_kwargs
)
return imagefile
# Registering the ThumbnailSizer to be available on VersatileImageField
# via the `thumbnail` attribute
versatileimagefield_registry.register_sizer('thumbnail', ThumbnailImage)]
All Sizers are registered via the versatileimagefield_registry.register_sizer
method. The first
argument is the attribute you want to make the Sizer available at and
the second is the SizedImage
subclass.
Filters are just as easy. Here’s how the InvertImage
filter is registered (see the highlighted lines in the following code block for the relevant bits):
import StringIO
from PIL import ImageOps
from .datastructures import FilteredImage
from .registry import versatileimagefield_registry
class InvertImage(FilteredImage):
"""
Inverts the colors of an image.
See the `process_image()` for more specifics
"""
def process_image(self, image, image_format, save_kwargs={}):
"""
Returns a StringIO instance of `image` with inverted colors
"""
imagefile = StringIO.StringIO()
inv_image = ImageOps.invert(image)
inv_image.save(
imagefile,
**save_kwargs
)
return imagefile
versatileimagefield_registry.register_filter('invert', InvertImage)
All Filters are registered via the
versatileimagefield_registry.register_filter
method. The first
argument is the attribute you want to make the Filter available at and
the second is the FilteredImage subclass.
Unallowed Sizer & Filter Names¶
Sizer and Filter names cannot begin with an underscore as it would
prevent them from being accessible within the template layer.
Additionally, since Sizers are available for use directly on a
VersatileImageField
, there are some Sizer names that are unallowed;
trying to register a Sizer with one of the following names will result
in a UnallowedSizerName
exception:
build_filters_and_sizers
chunks
close
closed
create_on_demand
delete
encoding
field
file
fileno
filters
flush
height
instance
isatty
multiple_chunks
name
newlines
open
path
ppoi
read
readinto
readline
readlines
save
seek
size
softspace
storage
tell
truncate
url
validate_ppoi
width
write
writelines
xreadlines
Overriding an existing Sizer or Filter¶
If you try to register a Sizer or Filter with an attribute name that’s
already in use (like crop
or thumbnail
or invert
), an
AlreadyRegistered
exception will raise.
Caution
A Sizer can have the same name as a Filter (since names are only required to be unique per type) however it’s not recommended.
If you’d like to override an already-registered Sizer or Filter just use
either the unregister_sizer
or unregister_filter
methods of
versatileimagefield_registry
. Here’s how you could ‘override’ the
crop
Sizer:
from versatileimagefield.registry import versatileimagefield_registry
# Unregistering the 'crop' Sizer
versatileimagefield_registry.unregister_sizer('crop')
# Registering a custom 'crop' Sizer
versatileimagefield_registry.register_sizer('crop', SomeCustomSizedImageCls)
The order that Sizers and Filters register corresponds to their
containing app’s position on INSTALLED_APPS
. This means that if you
want to override one of the default Sizers or Filters your app needs to
be included after 'versatileimagefield'
:
# settings.py
INSTALLED_APPS = (
'versatileimagefield',
'yourcustomapp' # This app can override the default Sizers and Filters
)