kopia lustrzana https://github.com/wagtail/wagtail
Merge branch 'feature/crop-closeness' of https://github.com/kaedroho/wagtail into kaedroho-feature/crop-closeness
commit
fb543589b7
|
@ -79,7 +79,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
|
||||
|
||||
|
@ -87,9 +87,25 @@ 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.
|
||||
|
||||
**The crop always aligns on the centre of the image.**
|
||||
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<percentage>`` 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).
|
||||
|
||||
``original``
|
||||
(takes no dimensions)
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
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
|
||||
|
||||
|
||||
class BaseImageBackend(object):
|
||||
|
@ -35,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.
|
||||
|
@ -68,9 +50,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 +74,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 +96,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,23 +112,139 @@ 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:
|
||||
resized_image = self.resize_to_min(image, size)
|
||||
return self.crop_to_centre(resized_image, size)
|
||||
crop_closeness = 0
|
||||
|
||||
# Get image width and height
|
||||
(im_width, im_height) = image.size
|
||||
|
||||
# Get filter width and height
|
||||
fl_width = size[0]
|
||||
fl_height = size[1]
|
||||
|
||||
# Get crop aspect ratio
|
||||
crop_aspect_ratio = fl_width / fl_height
|
||||
|
||||
# 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)
|
||||
|
||||
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:
|
||||
fp_x = focal_point.x
|
||||
fp_y = focal_point.y
|
||||
else:
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
|
||||
def no_operation(self, image, param, focal_point=None):
|
||||
"""Return the image unchanged"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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, initial=0)
|
||||
|
|
|
@ -278,6 +278,7 @@ class Filter(models.Model):
|
|||
# 'original'
|
||||
# 'width-200'
|
||||
# 'max-320x200'
|
||||
# 'fill-200x200-c50'
|
||||
|
||||
if self.spec == 'original':
|
||||
return Filter.OPERATION_NAMES['original'], None
|
||||
|
@ -286,6 +287,13 @@ class Filter(models.Model):
|
|||
if match:
|
||||
return Filter.OPERATION_NAMES[match.group(1)], int(match.group(2))
|
||||
|
||||
match = re.match(r'(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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<ul class="field-row">
|
||||
{% 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" %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -26,8 +26,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
|
||||
|
@ -951,70 +949,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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
)
|
Ładowanie…
Reference in New Issue