From e2fb0409819b28f833ccdc526cae92dac98175ac Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 9 Sep 2014 09:52:52 +0100 Subject: [PATCH 01/14] Implemented crop closeness --- wagtail/wagtailimages/backends/base.py | 58 ++++++++++++++++--- wagtail/wagtailimages/models.py | 9 ++- .../wagtailimages/images/results.html | 2 +- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/wagtail/wagtailimages/backends/base.py b/wagtail/wagtailimages/backends/base.py index a374f6ce1..11fb6eccb 100644 --- a/wagtail/wagtailimages/backends/base.py +++ b/wagtail/wagtailimages/backends/base.py @@ -1,6 +1,9 @@ +from __future__ import division + from django.conf import settings from wagtail.wagtailimages.utils import crop +from wagtail.wagtailimages.utils.focal_point import FocalPoint class BaseImageBackend(object): @@ -68,9 +71,9 @@ class BaseImageBackend(object): return image # scale factor if we were to downsize the image to fit the target width - horz_scale = float(target_width) / original_width + horz_scale = target_width / original_width # scale factor if we were to downsize the image to fit the target height - vert_scale = float(target_height) / original_height + vert_scale = target_height / original_height # choose whichever of these gives a smaller image if horz_scale < vert_scale: @@ -92,9 +95,9 @@ class BaseImageBackend(object): return image # scale factor if we were to downsize the image to fit the target width - horz_scale = float(target_width) / original_width + horz_scale = target_width / original_width # scale factor if we were to downsize the image to fit the target height - vert_scale = float(target_height) / original_height + vert_scale = target_height / original_height # choose whichever of these gives a larger image if horz_scale > vert_scale: @@ -114,7 +117,7 @@ class BaseImageBackend(object): if original_width <= target_width: return image - scale = float(target_width) / original_width + scale = target_width / original_width final_size = (target_width, int(original_height * scale)) @@ -130,20 +133,57 @@ class BaseImageBackend(object): if original_height <= target_height: return image - scale = float(target_height) / original_height + scale = target_height / original_height final_size = (int(original_width * scale), target_height) return self.resize(image, final_size) - def resize_to_fill(self, image, size, focal_point=None): + def resize_to_fill(self, image, arg, focal_point=None): """ Resize down and crop image to fill the given dimensions. Most suitable for thumbnails. (The final image will match the requested size, unless one or the other dimension is already smaller than the target size) """ - if focal_point is not None: - return self.crop_to_point(image, size, focal_point) + size = arg[:2] + + # Get crop closeness if it's set + if len(arg) > 2 and arg[2] is not None: + crop_closeness = arg[2] / 100 + + # Clamp it + if crop_closeness > 1: + crop_closeness = 1 + else: + crop_closeness = 0 + + print crop_closeness + + if focal_point is not None and crop_closeness > 0: + # Get focal point as a rect + left = focal_point.x - focal_point.width / 2 + top = focal_point.y - focal_point.height / 2 + right = focal_point.x + focal_point.width / 2 + bottom = focal_point.y + focal_point.height / 2 + + # Interpolate focal point rect with the images original size by crop closeness + # When crop_closeness = 0: new FP = image size + # When crop_closeness = 1: new FP = original FP size + (original_width, original_height) = image.size + left = left * crop_closeness + top = top * crop_closeness + right = (right - original_width) * crop_closeness + original_width + bottom = (bottom - original_height) * crop_closeness + original_height + + # Create new focal point + new_x = (left + right) / 2 + new_y = (top + bottom) / 2 + new_width = right - left + new_height = bottom - top + new_focal_point = FocalPoint(new_x, new_y, width=new_width, height=new_height) + + # Crop + return self.crop_to_point(image, size, new_focal_point) else: resized_image = self.resize_to_min(image, size) return self.crop_to_centre(resized_image, size) diff --git a/wagtail/wagtailimages/models.py b/wagtail/wagtailimages/models.py index 404cc7210..dbb3f1794 100644 --- a/wagtail/wagtailimages/models.py +++ b/wagtail/wagtailimages/models.py @@ -287,11 +287,18 @@ class Filter(models.Model): if match: return Filter.OPERATION_NAMES[match.group(1)], int(match.group(2)) + match = re.match(r'(max|min|fill)-(\d+)x(\d+)-c(\d+)$', self.spec) + if match: + width = int(match.group(2)) + height = int(match.group(3)) + crop_closeness = int(match.group(4)) + return Filter.OPERATION_NAMES[match.group(1)], (width, height, crop_closeness) + match = re.match(r'(max|min|fill)-(\d+)x(\d+)$', self.spec) if match: width = int(match.group(2)) height = int(match.group(3)) - return Filter.OPERATION_NAMES[match.group(1)], (width, height) + return Filter.OPERATION_NAMES[match.group(1)], (width, height, None) # Spec is not one of our recognised patterns raise Filter.InvalidFilterSpecError("Invalid image filter spec: %r" % self.spec) diff --git a/wagtail/wagtailimages/templates/wagtailimages/images/results.html b/wagtail/wagtailimages/templates/wagtailimages/images/results.html index 60380a7d8..1ea48be15 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/images/results.html +++ b/wagtail/wagtailimages/templates/wagtailimages/images/results.html @@ -17,7 +17,7 @@ {% for image in images %}
  • -
    {% image image max-165x165 %}
    +
    {% image image fill-165x165-c50 %}

    {{ image.title|ellipsistrim:60 }}

  • From b02d8db666b771d20fe53d097b03dc5724fbc2f6 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 9 Sep 2014 10:04:49 +0100 Subject: [PATCH 02/14] Fixed stray print statement --- wagtail/wagtailimages/backends/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/wagtail/wagtailimages/backends/base.py b/wagtail/wagtailimages/backends/base.py index 11fb6eccb..d0d5ba852 100644 --- a/wagtail/wagtailimages/backends/base.py +++ b/wagtail/wagtailimages/backends/base.py @@ -157,8 +157,6 @@ class BaseImageBackend(object): else: crop_closeness = 0 - print crop_closeness - if focal_point is not None and crop_closeness > 0: # Get focal point as a rect left = focal_point.x - focal_point.width / 2 From 046bd84f538ba7fa340091861b1630e86eed2023 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 9 Sep 2014 10:11:53 +0100 Subject: [PATCH 03/14] Don't process -c parameter on non fill filters --- wagtail/wagtailimages/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailimages/models.py b/wagtail/wagtailimages/models.py index dbb3f1794..054938e91 100644 --- a/wagtail/wagtailimages/models.py +++ b/wagtail/wagtailimages/models.py @@ -287,7 +287,7 @@ class Filter(models.Model): if match: return Filter.OPERATION_NAMES[match.group(1)], int(match.group(2)) - match = re.match(r'(max|min|fill)-(\d+)x(\d+)-c(\d+)$', self.spec) + match = re.match(r'(fill)-(\d+)x(\d+)-c(\d+)$', self.spec) if match: width = int(match.group(2)) height = int(match.group(3)) @@ -298,7 +298,7 @@ class Filter(models.Model): if match: width = int(match.group(2)) height = int(match.group(3)) - return Filter.OPERATION_NAMES[match.group(1)], (width, height, None) + return Filter.OPERATION_NAMES[match.group(1)], (width, height) # Spec is not one of our recognised patterns raise Filter.InvalidFilterSpecError("Invalid image filter spec: %r" % self.spec) From b1299427d8188c6ea98854581204a101972ee02a Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 9 Sep 2014 10:14:14 +0100 Subject: [PATCH 04/14] Added missing comment --- wagtail/wagtailimages/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wagtail/wagtailimages/models.py b/wagtail/wagtailimages/models.py index 054938e91..801113f3a 100644 --- a/wagtail/wagtailimages/models.py +++ b/wagtail/wagtailimages/models.py @@ -279,6 +279,7 @@ class Filter(models.Model): # 'original' # 'width-200' # 'max-320x200' + # 'fill-200x200-c50' if self.spec == 'original': return Filter.OPERATION_NAMES['original'], None From f9e8039b6a1974258e3d9a015bdfac428d58fd5b Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 9 Sep 2014 10:16:41 +0100 Subject: [PATCH 05/14] Removed an accidentally committed change (was used for testing) --- .../wagtailimages/templates/wagtailimages/images/results.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailimages/templates/wagtailimages/images/results.html b/wagtail/wagtailimages/templates/wagtailimages/images/results.html index 1ea48be15..60380a7d8 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/images/results.html +++ b/wagtail/wagtailimages/templates/wagtailimages/images/results.html @@ -17,7 +17,7 @@ {% for image in images %}
  • -
    {% image image fill-165x165-c50 %}
    +
    {% image image max-165x165 %}

    {{ image.title|ellipsistrim:60 }}

  • From 3f13d9d7e175fc40eb58e1b7260602a942325d7e Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 11 Sep 2014 14:29:02 +0100 Subject: [PATCH 06/14] Docs for crop closeness --- .../pages/writing_templates.rst | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/core_components/pages/writing_templates.rst b/docs/core_components/pages/writing_templates.rst index 3d1380115..37ea6663d 100644 --- a/docs/core_components/pages/writing_templates.rst +++ b/docs/core_components/pages/writing_templates.rst @@ -163,7 +163,7 @@ The available resizing methods are: Resize the height of the image to the dimension specified.. ``fill`` - (takes two dimensions) + (takes two dimensions and an optional ``-c`` parameter) .. code-block:: django @@ -171,9 +171,26 @@ The available resizing methods are: Resize and **crop** to fill the **exact** dimensions. - This can be particularly useful for websites requiring square thumbnails of arbitrary images. For example, a landscape image of width 2000, height 1000, treated with ``fill`` dimensions ``200x200`` would have its height reduced to 200, then its width (ordinarily 400) cropped to 200. + This can be particularly useful for websites requiring square thumbnails of arbitrary images. For example, a landscape image of width 2000, height 1000, treated with ``fill`` dimensions ``200x200`` would have its height reduced to 200, then its width (ordinarily 400) cropped to 200. + + This filter will crop to the images focal point if it has been set. If not, it will crop to the centre of the image. + + **Cropping closer to the focal point** + + By default, Wagtail will only crop to change the aspect ratio of the image. + + In some cases (thumbnails, for example) it may be nice to crop closer to the focal point so the subject of the image is easier to see. + + You can do this by appending ``-c`` at the end of the method. For example, if you would like the image to be cropped as closely as possible to its focal point, add ``-c100`` to the end of the method. + + .. code-block:: django + + {% image self.photo fill-200x200-c100 %} + + This will crop the image as much as it an but will never crop into the focal point. + + If you find that ``-c100`` is too close, you can try ``-c75`` or ``-c50`` (any whole number from 0 to 100 is accepted). - **The crop always aligns on the centre of the image.** ``original`` (takes no dimensions) From b7ac3042c4f535afdeb22bdfab3eba7eb949d0f3 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 12 Sep 2014 15:39:44 +0100 Subject: [PATCH 07/14] Added closeness box to URL generator --- wagtail/wagtailimages/forms.py | 1 + .../static/wagtailimages/js/image-url-generator.js | 13 ++++++++++++- .../wagtailimages/images/url_generator.html | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailimages/forms.py b/wagtail/wagtailimages/forms.py index af3b3ce83..27bd3c725 100644 --- a/wagtail/wagtailimages/forms.py +++ b/wagtail/wagtailimages/forms.py @@ -57,3 +57,4 @@ class URLGeneratorForm(forms.Form): ) width = forms.IntegerField(_("Width"), min_value=0) height = forms.IntegerField(_("Height"), min_value=0) + closeness = forms.IntegerField(_("Closeness"), min_value=0) diff --git a/wagtail/wagtailimages/static/wagtailimages/js/image-url-generator.js b/wagtail/wagtailimages/static/wagtailimages/js/image-url-generator.js index c9f8ed1e7..86bf41fcd 100644 --- a/wagtail/wagtailimages/static/wagtailimages/js/image-url-generator.js +++ b/wagtail/wagtailimages/static/wagtailimages/js/image-url-generator.js @@ -7,6 +7,7 @@ $(function() { var $filterMethodField = $form.find('select#id_filter_method'); var $widthField = $form.find('input#id_width'); var $heightField = $form.find('input#id_height'); + var $closenessField = $form.find('input#id_closeness'); var $result = $this.find('#result-url'); var $loadingMask = $this.find('.loading-mask') var $preview = $this.find('img.preview'); @@ -22,18 +23,28 @@ $(function() { if (filterSpec == 'original') { $widthField.prop('disabled', true); $heightField.prop('disabled', true); + $closenessField.prop('disabled', true); } else if (filterSpec == 'width') { $widthField.prop('disabled', false); $heightField.prop('disabled', true); + $closenessField.prop('disabled', true); filterSpec += '-' + $widthField.val(); } else if (filterSpec == 'height') { $widthField.prop('disabled', true); $heightField.prop('disabled', false); + $closenessField.prop('disabled', true); filterSpec += '-' + $heightField.val(); } else if (filterSpec == 'min' || filterSpec == 'max' || filterSpec == 'fill') { $widthField.prop('disabled', false); $heightField.prop('disabled', false); - filterSpec += '-' + $widthField.val() + 'x' + $heightField.val(); + + if (filterSpec == 'fill') { + $closenessField.prop('disabled', false); + filterSpec += '-' + $widthField.val() + 'x' + $heightField.val() + '-c' + $closenessField.val() + } else { + $closenessField.prop('disabled', true); + filterSpec += '-' + $widthField.val() + 'x' + $heightField.val(); + } } // Display note about scaled down images if image is large diff --git a/wagtail/wagtailimages/templates/wagtailimages/images/url_generator.html b/wagtail/wagtailimages/templates/wagtailimages/images/url_generator.html index aae28cd95..e725fd2f9 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/images/url_generator.html +++ b/wagtail/wagtailimages/templates/wagtailimages/images/url_generator.html @@ -17,6 +17,7 @@
      {% include "wagtailadmin/shared/field_as_li.html" with field=form.width li_classes="field-col col4" %} {% include "wagtailadmin/shared/field_as_li.html" with field=form.height li_classes="field-col col4" %} + {% include "wagtailadmin/shared/field_as_li.html" with field=form.closeness li_classes="field-col col4" %}
    From c974975590c91bef09a85c1551348e0ebba725fe Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 15 Sep 2014 17:33:34 +0100 Subject: [PATCH 08/14] Improved method for calculating the crop box on images with focal points --- wagtail/wagtailimages/backends/base.py | 91 +++++++++++++++++++------- 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/wagtail/wagtailimages/backends/base.py b/wagtail/wagtailimages/backends/base.py index d0d5ba852..0db17197b 100644 --- a/wagtail/wagtailimages/backends/base.py +++ b/wagtail/wagtailimages/backends/base.py @@ -133,7 +133,7 @@ class BaseImageBackend(object): if original_height <= target_height: return image - scale = target_height / original_height + scale = target_height / original_hight final_size = (int(original_width * scale), target_height) @@ -157,34 +157,75 @@ class BaseImageBackend(object): else: crop_closeness = 0 - if focal_point is not None and crop_closeness > 0: - # Get focal point as a rect - left = focal_point.x - focal_point.width / 2 - top = focal_point.y - focal_point.height / 2 - right = focal_point.x + focal_point.width / 2 - bottom = focal_point.y + focal_point.height / 2 + # Get image width and height + (im_width, im_height) = image.size - # Interpolate focal point rect with the images original size by crop closeness - # When crop_closeness = 0: new FP = image size - # When crop_closeness = 1: new FP = original FP size - (original_width, original_height) = image.size - left = left * crop_closeness - top = top * crop_closeness - right = (right - original_width) * crop_closeness + original_width - bottom = (bottom - original_height) * crop_closeness + original_height + # Get filter width and height + fl_width = size[0] + fl_height = size[1] - # Create new focal point - new_x = (left + right) / 2 - new_y = (top + bottom) / 2 - new_width = right - left - new_height = bottom - top - new_focal_point = FocalPoint(new_x, new_y, width=new_width, height=new_height) + # Get crop aspect ratio + crop_aspect_ratio = fl_width / fl_height - # Crop - return self.crop_to_point(image, size, new_focal_point) + # Get crop max + crop_max_scale = min(im_width, im_height * crop_aspect_ratio) + crop_max_width = crop_max_scale + crop_max_height = crop_max_scale / crop_aspect_ratio + + # Initialise crop width and height to max + crop_width = crop_max_width + crop_height = crop_max_height + + # Use crop closeness to zoom in + if focal_point is not None: + fp_width = focal_point.width + fp_height = focal_point.height + + # Get crop min + crop_min_scale = max(fp_width, fp_height * crop_aspect_ratio) + crop_min_width = crop_min_scale + crop_min_height = crop_min_scale / crop_aspect_ratio + + # Sometimes, the focal point may be bigger than the image... + if not crop_min_scale > crop_max_scale: + # Calculate max crop closeness to prevent upscaling + max_crop_closeness = max( + 1 - (fl_width - crop_min_width) / (crop_max_width - crop_min_width), + 1 - (fl_height - crop_min_height) / (crop_max_height - crop_min_height) + ) + + # Apply max crop closeness + crop_closeness = min(crop_closeness, max_crop_closeness) + + # Get crop width and height + crop_width = crop_max_width + (crop_min_width - crop_max_width) * crop_closeness + crop_height = crop_max_height + (crop_min_height - crop_max_height) * crop_closeness + + # Find focal point UV + if focal_point is not None: + fp_x = focal_point.x + fp_y = focal_point.y else: - resized_image = self.resize_to_min(image, size) - return self.crop_to_centre(resized_image, size) + # Fall back to positioning in the centre + fp_x = im_width / 2 + fp_y = im_height / 2 + + fp_u = fp_x / im_width + fp_v = fp_y / im_height + + # Position crop box based on focal point UV + crop_x = fp_x - (fp_u - 0.5) * crop_width + crop_y = fp_y - (fp_v - 0.5) * crop_height + + # Convert crop box into rect + left = crop_x - crop_width / 2 + top = crop_y - crop_height / 2 + right = crop_x + crop_width / 2 + bottom = crop_y + crop_height / 2 + + # Crop! + return self.resize_to_min(self.crop(image, crop.CropBox(left, top, right, bottom)), size) + def no_operation(self, image, param, focal_point=None): """Return the image unchanged""" From 7b4e8ca530b3ac35a66f890616d415de1defb70e Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 15 Sep 2014 17:37:55 +0100 Subject: [PATCH 09/14] Removed a bunch of no longer used code --- wagtail/wagtailimages/backends/base.py | 25 +---- wagtail/wagtailimages/backends/pillow.py | 4 +- wagtail/wagtailimages/backends/wand.py | 4 +- wagtail/wagtailimages/tests.py | 66 ------------- wagtail/wagtailimages/utils/crop.py | 121 ----------------------- wagtail/wagtailimages/utils/rect.py | 38 +++++++ 6 files changed, 44 insertions(+), 214 deletions(-) delete mode 100644 wagtail/wagtailimages/utils/crop.py create mode 100644 wagtail/wagtailimages/utils/rect.py diff --git a/wagtail/wagtailimages/backends/base.py b/wagtail/wagtailimages/backends/base.py index 0db17197b..401733ae9 100644 --- a/wagtail/wagtailimages/backends/base.py +++ b/wagtail/wagtailimages/backends/base.py @@ -2,7 +2,7 @@ from __future__ import division from django.conf import settings -from wagtail.wagtailimages.utils import crop +from wagtail.wagtailimages.utils.rect import Rect from wagtail.wagtailimages.utils.focal_point import FocalPoint @@ -38,27 +38,6 @@ class BaseImageBackend(object): def crop(self, image, crop_box): raise NotImplementedError('subclasses of BaseImageBackend must provide a crop() method') - def crop_to_centre(self, image, size): - crop_box = crop.crop_to_centre(image.size, size) - if crop_box.size != image.size: - return self.crop(image, crop_box) - else: - return image - - def crop_to_point(self, image, size, focal_point): - crop_box = crop.crop_to_point(image.size, size, focal_point) - - # Don't crop if we don't need to - if crop_box.size != image.size: - image = self.crop(image, crop_box) - - # If the focal points are too large, the cropping system may not - # crop it fully, resize the image if this has happened: - if crop_box.size != size: - image = self.resize_to_fill(image, size) - - return image - def resize_to_max(self, image, size, focal_point=None): """ Resize image down to fit within the given dimensions, preserving aspect ratio. @@ -224,7 +203,7 @@ class BaseImageBackend(object): bottom = crop_y + crop_height / 2 # Crop! - return self.resize_to_min(self.crop(image, crop.CropBox(left, top, right, bottom)), size) + return self.resize_to_min(self.crop(image, Rect(left, top, right, bottom)), size) def no_operation(self, image, param, focal_point=None): diff --git a/wagtail/wagtailimages/backends/pillow.py b/wagtail/wagtailimages/backends/pillow.py index a1963f931..c460c7d2c 100644 --- a/wagtail/wagtailimages/backends/pillow.py +++ b/wagtail/wagtailimages/backends/pillow.py @@ -21,8 +21,8 @@ class PillowBackend(BaseImageBackend): image = image.convert('RGB') return image.resize(size, PIL.Image.ANTIALIAS) - def crop(self, image, crop_box): - return image.crop(crop_box) + def crop(self, image, rect): + return image.crop(rect) def image_data_as_rgb(self, image): # https://github.com/thumbor/thumbor/blob/f52360dc96eedd9fc914fcf19eaf2358f7e2480c/thumbor/engines/pil.py#L206-L215 diff --git a/wagtail/wagtailimages/backends/wand.py b/wagtail/wagtailimages/backends/wand.py index 3c41f60c1..d6c9c3048 100644 --- a/wagtail/wagtailimages/backends/wand.py +++ b/wagtail/wagtailimages/backends/wand.py @@ -25,10 +25,10 @@ class WandBackend(BaseImageBackend): new_image.resize(size[0], size[1]) return new_image - def crop(self, image, crop_box): + def crop(self, image, rect): new_image = image.clone() new_image.crop( - left=crop_box[0], top=crop_box[1], right=crop_box[2], bottom=crop_box[3] + left=rect[0], top=rect[1], right=rect[2], bottom=rect[3] ) return new_image diff --git a/wagtail/wagtailimages/tests.py b/wagtail/wagtailimages/tests.py index 06ce8c9a8..389e7c58f 100644 --- a/wagtail/wagtailimages/tests.py +++ b/wagtail/wagtailimages/tests.py @@ -25,8 +25,6 @@ from wagtail.wagtailimages.formats import ( from wagtail.wagtailimages.backends import get_image_backend from wagtail.wagtailimages.backends.pillow import PillowBackend -from wagtail.wagtailimages.utils.crop import crop_to_point, CropBox -from wagtail.wagtailimages.utils.focal_point import FocalPoint from wagtail.wagtailimages.utils.crypto import generate_signature, verify_signature from wagtail.tests.models import EventPage, EventPageCarouselItem from wagtail.wagtailcore.models import Page @@ -953,70 +951,6 @@ class TestGenerateURLView(TestCase, WagtailTestUtils): })) -class TestCropToPoint(TestCase): - def test_basic(self): - "Test basic cropping in the centre of the image" - self.assertEqual( - crop_to_point((640, 480), (100, 100), FocalPoint(x=320, y=240)), - CropBox(270, 190, 370, 290), - ) - - def test_basic_no_focal_point(self): - "If focal point is None, it should make one in the centre of the image" - self.assertEqual( - crop_to_point((640, 480), (100, 100), None), - CropBox(270, 190, 370, 290), - ) - - def test_doesnt_exit_top_left(self): - "Test that the cropbox doesn't exit the image at the top left" - self.assertEqual( - crop_to_point((640, 480), (100, 100), FocalPoint(x=0, y=0)), - CropBox(0, 0, 100, 100), - ) - - def test_doesnt_exit_bottom_right(self): - "Test that the cropbox doesn't exit the image at the bottom right" - self.assertEqual( - crop_to_point((640, 480), (100, 100), FocalPoint(x=640, y=480)), - CropBox(540, 380, 640, 480), - ) - - def test_doesnt_get_smaller_than_focal_point(self): - "Test that the cropbox doesn't get any smaller than the focal point" - self.assertEqual( - crop_to_point((640, 480), (10, 10), FocalPoint(x=320, y=240, width=100, height=100)), - CropBox(270, 190, 370, 290), - ) - - def test_keeps_composition(self): - "Test that the cropbox tries to keep the composition of the original image as much as it can" - self.assertEqual( - crop_to_point((300, 300), (150, 150), FocalPoint(x=100, y=200)), - CropBox(50, 100, 200, 250), # Focal point is 1/3 across and 2/3 down in the crop box - ) - - def test_keeps_focal_point_in_view_bottom_left(self): - """ - Even though it tries to keep the composition of the image, - it shouldn't let that get in the way of keeping the entire subject in view - """ - self.assertEqual( - crop_to_point((300, 300), (150, 150), FocalPoint(x=100, y=200, width=150, height=150)), - CropBox(25, 125, 175, 275), - ) - - def test_keeps_focal_point_in_view_top_right(self): - """ - Even though it tries to keep the composition of the image, - it shouldn't let that get in the way of keeping the entire subject in view - """ - self.assertEqual( - crop_to_point((300, 300), (150, 150), FocalPoint(x=200, y=100, width=150, height=150)), - CropBox(125, 25, 275, 175), - ) - - class TestIssue573(TestCase): """ This tests for a bug which causes filename limit on Renditions to be reached diff --git a/wagtail/wagtailimages/utils/crop.py b/wagtail/wagtailimages/utils/crop.py deleted file mode 100644 index 983f5db88..000000000 --- a/wagtail/wagtailimages/utils/crop.py +++ /dev/null @@ -1,121 +0,0 @@ -from __future__ import division - -from wagtail.wagtailimages.utils.focal_point import FocalPoint - - -class CropBox(object): - def __init__(self, left, top, right, bottom): - self.left = int(left) - self.top = int(top) - self.right = int(right) - self.bottom = int(bottom) - - def __getitem__(self, key): - return (self.left, self.top, self.right, self.bottom)[key] - - @property - def width(self): - return self.right - self.left - - @property - def height(self): - return self.bottom - self.top - - @property - def size(self): - return self.width, self.height - - def as_tuple(self): - return self.left, self.top, self.right, self.bottom - - def __eq__(self, other): - return self.as_tuple() == other.as_tuple() - - def __ne__(self, other): - return not (self == other) - - def __repr__(self): - return 'CropBox(left: %d, top: %d, right: %d, bottom: %d)' % ( - self.left, self.top, self.right, self.bottom - ) - - -def crop_to_centre(image_size, crop_size): - (original_width, original_height) = image_size - (crop_width, crop_height) = crop_size - - # final dimensions should not exceed original dimensions - final_width = min(original_width, crop_width) - final_height = min(original_height, crop_height) - - left = (original_width - final_width) / 2 - top = (original_height - final_height) / 2 - - return CropBox(left, top, left + final_width, top + final_height) - - -def crop_to_point(image_size, crop_size, focal_point): - (original_width, original_height) = image_size - (crop_width, crop_height) = crop_size - - if not focal_point: - focal_point = FocalPoint(original_width / 2, original_height / 2) - - # Make sure that the crop size is no smaller than the focal point - crop_width = max(crop_width, focal_point.width) - crop_height = max(crop_height, focal_point.height) - - # Make sure final dimensions do not exceed original dimensions - final_width = min(original_width, crop_width) - final_height = min(original_height, crop_height) - - # Get UV for focal point - focal_point_u = focal_point.x / original_width - focal_point_v = focal_point.y / original_height - - # Get crop box - left = focal_point.x - focal_point_u * final_width - top = focal_point.y - focal_point_v * final_height - right = focal_point.x - focal_point_u * final_width + final_width - bottom = focal_point.y - focal_point_v * final_height + final_height - - # Make sure the entire focal point is in the crop box - focal_point_left = focal_point.x - focal_point.width / 2 - focal_point_top = focal_point.y - focal_point.height / 2 - focal_point_right = focal_point.x + focal_point.width / 2 - focal_point_bottom = focal_point.y + focal_point.height / 2 - - if left > focal_point_left: - right -= left - focal_point_left - left = focal_point_left - - if top > focal_point_top: - bottom -= top - focal_point_top - top = focal_point_top - - if right < focal_point_right: - left += focal_point_right - right; - right = focal_point_right - - if bottom < focal_point_bottom: - top += focal_point_bottom - bottom; - bottom = focal_point_bottom - - # Don't allow the crop box to go over the image boundary - if left < 0: - right -= left - left = 0 - - if top < 0: - bottom -= top - top = 0 - - if right > original_width: - left -= right - original_width - right = original_width - - if bottom > original_height: - top -= bottom - original_height - bottom = original_height - - return CropBox(left, top, right, bottom) diff --git a/wagtail/wagtailimages/utils/rect.py b/wagtail/wagtailimages/utils/rect.py new file mode 100644 index 000000000..5140ce0bb --- /dev/null +++ b/wagtail/wagtailimages/utils/rect.py @@ -0,0 +1,38 @@ +from __future__ import division + + +class Rect(object): + def __init__(self, left, top, right, bottom): + self.left = int(left) + self.top = int(top) + self.right = int(right) + self.bottom = int(bottom) + + def __getitem__(self, key): + return (self.left, self.top, self.right, self.bottom)[key] + + @property + def width(self): + return self.right - self.left + + @property + def height(self): + return self.bottom - self.top + + @property + def size(self): + return self.width, self.height + + def as_tuple(self): + return self.left, self.top, self.right, self.bottom + + def __eq__(self, other): + return self.as_tuple() == other.as_tuple() + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return 'Rect(left: %d, top: %d, right: %d, bottom: %d)' % ( + self.left, self.top, self.right, self.bottom + ) From b68c845a6668859b0678e9f78286127da3e4878c Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 16 Sep 2014 08:53:02 +0100 Subject: [PATCH 10/14] Set initial closeness value to 0 --- wagtail/wagtailimages/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailimages/forms.py b/wagtail/wagtailimages/forms.py index 27bd3c725..46bdd997d 100644 --- a/wagtail/wagtailimages/forms.py +++ b/wagtail/wagtailimages/forms.py @@ -57,4 +57,4 @@ class URLGeneratorForm(forms.Form): ) width = forms.IntegerField(_("Width"), min_value=0) height = forms.IntegerField(_("Height"), min_value=0) - closeness = forms.IntegerField(_("Closeness"), min_value=0) + closeness = forms.IntegerField(_("Closeness"), min_value=0, initial=0) From 4757c1633c2680d057a6cbee7b122f8ebf492103 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 16 Sep 2014 15:10:40 +0100 Subject: [PATCH 11/14] Make sure the entire focal point is in the crop --- wagtail/wagtailimages/backends/base.py | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/wagtail/wagtailimages/backends/base.py b/wagtail/wagtailimages/backends/base.py index 401733ae9..3d7d4957a 100644 --- a/wagtail/wagtailimages/backends/base.py +++ b/wagtail/wagtailimages/backends/base.py @@ -202,6 +202,46 @@ class BaseImageBackend(object): right = crop_x + crop_width / 2 bottom = crop_y + crop_height / 2 + # Make sure the entire focal point is in the crop box + if focal_point is not None: + focal_point_left = focal_point.x - focal_point.width / 2 + focal_point_top = focal_point.y - focal_point.height / 2 + focal_point_right = focal_point.x + focal_point.width / 2 + focal_point_bottom = focal_point.y + focal_point.height / 2 + + if left > focal_point_left: + right -= left - focal_point_left + left = focal_point_left + + if top > focal_point_top: + bottom -= top - focal_point_top + top = focal_point_top + + if right < focal_point_right: + left += focal_point_right - right; + right = focal_point_right + + if bottom < focal_point_bottom: + top += focal_point_bottom - bottom; + bottom = focal_point_bottom + + # Don't allow the crop box to go over the image boundary + if left < 0: + right -= left + left = 0 + + if top < 0: + bottom -= top + top = 0 + + if right > im_width: + left -= right - im_width + right = im_width + + if bottom > im_height: + top -= bottom - im_height + bottom = im_height + # Crop! return self.resize_to_min(self.crop(image, Rect(left, top, right, bottom)), size) From 9e3bec0d5f9ae636e31750035f065be325e260c3 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 16 Sep 2014 15:27:01 +0100 Subject: [PATCH 12/14] Don't use crop closeness if it's out of range --- wagtail/wagtailimages/backends/base.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/wagtail/wagtailimages/backends/base.py b/wagtail/wagtailimages/backends/base.py index 3d7d4957a..78e4443af 100644 --- a/wagtail/wagtailimages/backends/base.py +++ b/wagtail/wagtailimages/backends/base.py @@ -176,9 +176,10 @@ class BaseImageBackend(object): # Apply max crop closeness crop_closeness = min(crop_closeness, max_crop_closeness) - # Get crop width and height - crop_width = crop_max_width + (crop_min_width - crop_max_width) * crop_closeness - crop_height = crop_max_height + (crop_min_height - crop_max_height) * crop_closeness + if 1 >= crop_closeness >= 0: + # Get crop width and height + crop_width = crop_max_width + (crop_min_width - crop_max_width) * crop_closeness + crop_height = crop_max_height + (crop_min_height - crop_max_height) * crop_closeness # Find focal point UV if focal_point is not None: @@ -243,7 +244,7 @@ class BaseImageBackend(object): bottom = im_height # Crop! - return self.resize_to_min(self.crop(image, Rect(left, top, right, bottom)), size) + return self.resize_to_min(self.crop(image, Rect(left, top, right, bottom)), (100, 100)) def no_operation(self, image, param, focal_point=None): From cd3431a57e60d68e92cbc3c961f0cbbcca2c1539 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 16 Sep 2014 15:29:38 +0100 Subject: [PATCH 13/14] Fixed some accidentally committed code --- wagtail/wagtailimages/backends/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailimages/backends/base.py b/wagtail/wagtailimages/backends/base.py index 78e4443af..b19052c5c 100644 --- a/wagtail/wagtailimages/backends/base.py +++ b/wagtail/wagtailimages/backends/base.py @@ -244,7 +244,7 @@ class BaseImageBackend(object): bottom = im_height # Crop! - return self.resize_to_min(self.crop(image, Rect(left, top, right, bottom)), (100, 100)) + return self.resize_to_min(self.crop(image, Rect(left, top, right, bottom)), size) def no_operation(self, image, param, focal_point=None): From 23bbc1bcf951a76c7bf46aa464278107e0bbe69b Mon Sep 17 00:00:00 2001 From: Dan Braghis Date: Wed, 17 Sep 2014 16:17:02 +0100 Subject: [PATCH 14/14] Fix typo and slight PEP8 cleanup. --- wagtail/wagtailimages/backends/base.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/wagtail/wagtailimages/backends/base.py b/wagtail/wagtailimages/backends/base.py index b19052c5c..7c95f2612 100644 --- a/wagtail/wagtailimages/backends/base.py +++ b/wagtail/wagtailimages/backends/base.py @@ -112,7 +112,7 @@ class BaseImageBackend(object): if original_height <= target_height: return image - scale = target_height / original_hight + scale = target_height / original_height final_size = (int(original_width * scale), target_height) @@ -219,11 +219,11 @@ class BaseImageBackend(object): top = focal_point_top if right < focal_point_right: - left += focal_point_right - right; + left += focal_point_right - right right = focal_point_right if bottom < focal_point_bottom: - top += focal_point_bottom - bottom; + top += focal_point_bottom - bottom bottom = focal_point_bottom # Don't allow the crop box to go over the image boundary @@ -246,7 +246,6 @@ class BaseImageBackend(object): # Crop! return self.resize_to_min(self.crop(image, Rect(left, top, right, bottom)), size) - def no_operation(self, image, param, focal_point=None): """Return the image unchanged""" return image