Specifying a Primary Point of Interest (PPOI)¶
The crop Sizer is super-useful for creating images at a specific
size/aspect-ratio however, sometimes you want the ‘crop centerpoint’ to
be somewhere other than the center of a particular image. In fact, the
initial inspiration for
django-versatileimagefield came as a result
of tackling this very problem.
crop Sizer’s core functionality (located in the
versatileimagefield.versatileimagefield.CroppedImage.crop_on_centerpoint method) was inspired by PIL’s
function (by Kevin Cazabon) which takes an optional
centering, that expects a 2-tuple comprised of
floats which are greater than or equal to 0 and less than or equal to 1. These two values
together form a cartesian coordinate system which dictates the percentage of pixels to ‘trim’ off each of the long sides (i.e. left/right or top/bottom, depending on the aspect ratio of the cropped size vs. the original size):
crop Sizer works in a similar way but converts the 2-tuple into an exact (x, y) pixel coordinate which is then used as the ‘centerpoint’ of the crop. This approach gives significantly more accurate results than using
ImageOps.fit, especially when dealing with PPOI values located near the edges of an image or aspect ratios that differ significantly from the original image.
Even though the PPOI value is used as a crop ‘centerpoint’, the pixel it corresponds to won’t necessarily be in the center of the cropped image, especially if its near the edges of the original image.
At present, only the
crop Sizer changes how it creates images
based on PPOI but a
VersatileImageField makes its PPOI value
available to ALL its attached Filters and Sizers. Get creative!
Each image managed by a
VersatileImageField can store its own,
unique PPOI in the database via the easy-to-use
how to integrate it into our example model (relevant lines highlighted in the code block below):
# models.py with `VersatileImageField` & `PPOIField` from django.db import models from versatileimagefield.fields import VersatileImageField, \ PPOIField class ImageExampleModel(models.Model): name = models.CharField( 'Name', max_length=80 ) image = VersatileImageField( 'Image', upload_to='images/testimagemodel/', width_field='width', height_field='height', ppoi_field='ppoi' ) height = models.PositiveIntegerField( 'Image Height', blank=True, null=True ) width = models.PositiveIntegerField( 'Image Width', blank=True, null=True ) ppoi = PPOIField( 'Image PPOI' ) class Meta: verbose_name = 'Image Example' verbose_name_plural = 'Image Examples'
As you can see, you’ll need to add a new
PPOIField field to your
model and then include the name of that field in the
ppoi_field keyword argument. That’s it!
PPOIField is fully-compatible with
migrate to your heart’s content!
How PPOI is Stored in the Database¶
The Primary Point of Interest is stored in the database as a string
with the x and y coordinates limited to two decimal places and separated
by an ‘x’ (for instance:
PPOI is set via the
ppoi attribute on a
When you save a model instance,
VersatileImageField will ensure its
currently-assigned PPOI value is ‘sent’ to the
with it (if any) prior to writing to the database.
Via The Shell¶
# Importing our example Model >>> from someapp.models import ImageExampleModel # Retrieving a model instance >>> example = ImageExampleModel.objects.all() # Retrieving the current PPOI value associated with the image field # A `VersatileImageField`'s PPOI value is ALWAYS associated with the `ppoi` # attribute, irregardless of what you named the `PPOIField` attribute on your model >>> example.image.ppoi (0.5, 0.5) # Creating a cropped image >>> example.image.crop['400x400'].url u'/media/__sized__/images/testimagemodel/test-image-crop-c0-5__0-5-400x400.jpg' # Changing the PPOI value >>> example.image.ppoi = (1, 1) # Creating a new cropped image with the new PPOI value >>> example.image.crop['400x400'].url u'/media/__sized__/images/testimagemodel/test-image-crop-c1__1-400x400.jpg' # PPOI values can be set as either a tuple or a string >>> example.image.ppoi = '0.1x0.55' >>> example.image.ppoi (0.1, 0.55) >>> example.image.ppoi = (0.75, 0.25) >>> example.image.crop['400x400'].url u'/media/__sized__/images/testimagemodel/test-image-crop-c0-75__0-25-400x400.jpg' # u'0.75x0.25' is written to the database in the 'ppoi' column associated with # our example model >>> example.save()
As you can see, changing an image’s PPOI changes the filename of the
cropped image. This ensures updates to a
value will result in unique cache entries for each unique image it
Each time a field’s PPOI is set, its attached Filters & Sizers will be immediately updated with the new value.
It’s pretty hard to accurately set a particular image’s PPOI when
working in the Python shell so
django-versatileimagefield ships with
an admin-ready formfield. Simply add an image, click ‘Save and continue
editing’, click where you’d like the PPOI to be and then save your model
instance again. A helpful translucent red square will indicate where the
PPOI value is currently set to on the image:
PPOIField is not editable so it will be automatically excluded from the admin.
Django 1.5 Admin Integration for required
If you’re using a required (i.e.
VersatileImageField on a project running Django 1.5 you’ll need a custom form class to circumvent an already-fixed-in-Django-1.6 issue (that has to do with required fields associated with a
MultiWidget used in a
The example below uses an example model
YourModel that has a required
VersatileImageField as the
# yourapp/forms.py from django.forms import ModelForm from versatileimagefield.fields import SizedImageCenterpointClickDjangoAdminField from .models import YourModel class YourModelForm(VersatileImageTestModelForm): image = SizedImageCenterpointClickDjangoAdminField(required=False) class Meta: model = YourModel fields = ('image',)
required=False in the formfield definition in the above example.
Integrating the custom form into the admin:
# yourapp/admin.py from django.contrib import admin from .forms import YourModelForm from .models import YourModel class YourModelAdmin(admin.ModelAdmin): form = YourModelForm admin.site.register(YourModel, YourModelAdmin)