[IMP] tools,base,web,website_*: keep image ratio on resize
* = website_partner, website_profile, website_sale, website_slides
Before
======
Since commit: 2e3848b394
The images that are resized have additional borders if the target ratio is
different than the image ratio. Those borders are transparent if the image
format supports it, and are white otherwise.
With that current solution, if the background where the image is displayed is
another color than white, it is looking really bad.
Moreover, most images are stored resized like this, so it is not even possible
to decide if it should have borders or not depending on the context, the
original image and ratio is forever lost.
It is also inconsistent because if the image is already smaller than the target
size then it doesn't include borders. In that case it keeps the original ratio
instead of the target ratio. So it isn't even guaranteed that the target
ratio is going to be respected.
After
=====
This commit will solve all of those problems by always keeping the ratio of the
original image.
This implies the views should be taking care of adding borders when necessary.
task-1958000
PR: #31811
This commit is contained in:
@@ -168,7 +168,7 @@ class ProductProduct(models.Model):
|
||||
for record in self:
|
||||
images = tools.image_get_resized_images(record.image_raw_original, big_name=False)
|
||||
record.image_raw_big = tools.image_get_resized_images(record.image_raw_original,
|
||||
large_name=False, medium_name=False, small_name=False, preserve_aspect_ratio=True)['image']
|
||||
large_name=False, medium_name=False, small_name=False)['image']
|
||||
record.image_raw_large = images['image_large']
|
||||
record.image_raw_medium = images['image_medium']
|
||||
record.image_raw_small = images['image_small']
|
||||
|
||||
@@ -1023,8 +1023,8 @@ class Binary(http.Controller):
|
||||
'/web/image/<int:id>-<string:unique>/<int:width>x<int:height>/<string:filename>'], type='http', auth="public")
|
||||
def content_image(self, xmlid=None, model='ir.attachment', id=None, field='datas',
|
||||
filename_field='datas_fname', unique=None, filename=None, mimetype=None,
|
||||
download=None, width=0, height=0, crop=False, access_token=None, avoid_if_small=False,
|
||||
upper_limit=False, placeholder='placeholder.png', **kw):
|
||||
download=None, width=0, height=0, crop=False, access_token=None,
|
||||
placeholder='placeholder.png', **kwargs):
|
||||
status, headers, image_base64 = request.env['ir.http'].binary_content(
|
||||
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
|
||||
filename_field=filename_field, download=download, mimetype=mimetype,
|
||||
@@ -1041,7 +1041,7 @@ class Binary(http.Controller):
|
||||
image_base64 = getattr(odoo.tools, 'image_resize_image_%s' % suffix)(image_base64)
|
||||
|
||||
image_base64 = limited_image_resize(
|
||||
image_base64, width=width, height=height, crop=crop, upper_limit=upper_limit, avoid_if_small=avoid_if_small)
|
||||
image_base64, width=width, height=height, crop=crop)
|
||||
|
||||
content = base64.b64decode(image_base64)
|
||||
headers.append(('Content-Length', len(content)))
|
||||
|
||||
@@ -45,7 +45,6 @@ class Image(models.AbstractModel):
|
||||
|
||||
sha = hashlib.sha1(str(getattr(record, '__last_update')).encode('utf-8')).hexdigest()[0:7]
|
||||
max_size = '' if max_size is None else '/%s' % max_size
|
||||
avoid_if_small = '&avoid_if_small=true' if options.get('avoid_if_small') else ''
|
||||
|
||||
if options.get('filename-field') and getattr(record, options['filename-field'], None):
|
||||
filename = record[options['filename-field']]
|
||||
@@ -54,7 +53,7 @@ class Image(models.AbstractModel):
|
||||
else:
|
||||
filename = record.display_name
|
||||
|
||||
src = '/web/image/%s/%s/%s%s/%s?unique=%s%s' % (record._name, record.id, options.get('preview_image', field_name), max_size, url_quote(filename), sha, avoid_if_small)
|
||||
src = '/web/image/%s/%s/%s%s/%s?unique=%s' % (record._name, record.id, options.get('preview_image', field_name), max_size, url_quote(filename), sha)
|
||||
|
||||
if options.get('alt-field') and getattr(record, options['alt-field'], None):
|
||||
alt = escape(record[options['alt-field']])
|
||||
|
||||
@@ -13,10 +13,10 @@ class TestImage(HttpCase):
|
||||
"""The goal of this test is to make sure the placeholder image is
|
||||
resized appropriately depending on the given URL parameters."""
|
||||
|
||||
# CASE: resize placeholder to given size
|
||||
# CASE: resize placeholder, given size but original ratio is always kept
|
||||
response = self.url_open('/web/image/0/200x150')
|
||||
image = Image.open(io.BytesIO(response.content))
|
||||
self.assertEqual(image.size, (200, 150))
|
||||
self.assertEqual(image.size, (150, 150))
|
||||
|
||||
# CASE: resize placeholder to small
|
||||
response = self.url_open('/web/image/fake/0/image_small')
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<template id="partner_detail" name="Partner Details">
|
||||
<h1 class="col-lg-12 text-center" id="partner_name" t-field="partner.display_name"/>
|
||||
<div class="col-lg-4">
|
||||
<div t-field="partner.image" t-options='{"widget": "image", "class": "d-block mx-auto mb16", "max_width": 512, "avoid_if_small": True}'/>
|
||||
<div t-field="partner.image" t-options='{"widget": "image", "class": "d-block mx-auto mb16", "max_width": 512}'/>
|
||||
<address>
|
||||
<div t-field="partner.self" t-options='{
|
||||
"widget": "contact",
|
||||
|
||||
@@ -80,7 +80,7 @@ class WebsiteProfile(http.Controller):
|
||||
@http.route([
|
||||
'/profile/avatar/<int:user_id>',
|
||||
], type='http', auth="public", website=True, sitemap=False)
|
||||
def get_user_profile_avatar(self, user_id, field='image_large', width=0, height=0, crop=False, avoid_if_small=False, upper_limit=False, **post):
|
||||
def get_user_profile_avatar(self, user_id, field='image_large', width=0, height=0, crop=False, **post):
|
||||
if field not in ('image_small', 'image_medium', 'image_large'):
|
||||
return werkzeug.exceptions.Forbidden()
|
||||
|
||||
@@ -102,7 +102,7 @@ class WebsiteProfile(http.Controller):
|
||||
image_base64 = self._get_default_avatar(field, headers, width, height)
|
||||
|
||||
image_base64 = tools.limited_image_resize(
|
||||
image_base64, width=width, height=height, crop=crop, upper_limit=upper_limit, avoid_if_small=avoid_if_small)
|
||||
image_base64, width=width, height=height, crop=crop)
|
||||
|
||||
content = base64.b64decode(image_base64)
|
||||
headers.append(('Content-Length', len(content)))
|
||||
|
||||
@@ -167,37 +167,37 @@ class TestWebsiteSaleImage(odoo.tests.HttpCase):
|
||||
self.assertEqual(product_red.image_big, product_red.image)
|
||||
self.assertEqual(product_green.image_big, product_green.image)
|
||||
|
||||
# Verify large size
|
||||
# Verify large size: keep aspect ratio
|
||||
image = Image.open(io.BytesIO(base64.b64decode(template.image_large)))
|
||||
self.assertEqual(image.size, (256, 256))
|
||||
self.assertEqual(image.size, (256, 144))
|
||||
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), (65, 105, 225), "blue")
|
||||
image = Image.open(io.BytesIO(base64.b64decode(product_red.image_large)))
|
||||
self.assertEqual(image.size, (256, 256))
|
||||
self.assertEqual(image.size, (256, 160))
|
||||
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), (205, 92, 92), "red")
|
||||
image = Image.open(io.BytesIO(base64.b64decode(product_green.image_large)))
|
||||
self.assertEqual(image.size, (256, 256))
|
||||
self.assertEqual(image.size, (256, 144))
|
||||
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), (34, 139, 34), "green")
|
||||
|
||||
# Verify medium size
|
||||
# Verify medium size: keep aspect ratio
|
||||
image = Image.open(io.BytesIO(base64.b64decode(template.image_medium)))
|
||||
self.assertEqual(image.size, (128, 128))
|
||||
self.assertEqual(image.size, (128, 72))
|
||||
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), (65, 105, 225), "blue")
|
||||
image = Image.open(io.BytesIO(base64.b64decode(product_red.image_medium)))
|
||||
self.assertEqual(image.size, (128, 128))
|
||||
self.assertEqual(image.size, (128, 80))
|
||||
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), (205, 92, 92), "red")
|
||||
image = Image.open(io.BytesIO(base64.b64decode(product_green.image_medium)))
|
||||
self.assertEqual(image.size, (128, 128))
|
||||
self.assertEqual(image.size, (128, 72))
|
||||
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), (34, 139, 34), "green")
|
||||
|
||||
# Verify small size
|
||||
# Verify small size: keep aspect ratio
|
||||
image = Image.open(io.BytesIO(base64.b64decode(template.image_small)))
|
||||
self.assertEqual(image.size, (64, 64))
|
||||
self.assertEqual(image.size, (64, 36))
|
||||
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), (65, 105, 225), "blue")
|
||||
image = Image.open(io.BytesIO(base64.b64decode(product_red.image_small)))
|
||||
self.assertEqual(image.size, (64, 64))
|
||||
self.assertEqual(image.size, (64, 40))
|
||||
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), (205, 92, 92), "red")
|
||||
image = Image.open(io.BytesIO(base64.b64decode(product_green.image_small)))
|
||||
self.assertEqual(image.size, (64, 64))
|
||||
self.assertEqual(image.size, (64, 36))
|
||||
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), (34, 139, 34), "green")
|
||||
|
||||
# self.env.cr.commit() # uncomment to save the product to test in browser
|
||||
|
||||
@@ -506,7 +506,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
return response
|
||||
|
||||
@http.route('/slides/slide/<int:slide_id>/get_image', type='http', auth="public", website=True, sitemap=False)
|
||||
def slide_get_image(self, slide_id, field='image_medium', width=0, height=0, crop=False, avoid_if_small=False, upper_limit=False):
|
||||
def slide_get_image(self, slide_id, field='image_medium', width=0, height=0, crop=False):
|
||||
# Protect infographics by limiting access to 256px (large) images
|
||||
if field not in ('image_small', 'image_medium', 'image_large'):
|
||||
return werkzeug.exceptions.Forbidden()
|
||||
@@ -527,7 +527,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
image_base64 = self._get_default_avatar(field, headers, width, height)
|
||||
|
||||
image_base64 = tools.limited_image_resize(
|
||||
image_base64, width=width, height=height, crop=crop, upper_limit=upper_limit, avoid_if_small=avoid_if_small)
|
||||
image_base64, width=width, height=height, crop=crop)
|
||||
|
||||
content = base64.b64decode(image_base64)
|
||||
headers.append(('Content-Length', len(content)))
|
||||
|
||||
@@ -28,7 +28,7 @@ class ImageMixin(models.AbstractModel):
|
||||
for record in self:
|
||||
images = tools.image_get_resized_images(record.image_original, big_name=False)
|
||||
record.image_big = tools.image_get_resized_images(record.image_original,
|
||||
large_name=False, medium_name=False, small_name=False, preserve_aspect_ratio=True)['image']
|
||||
large_name=False, medium_name=False, small_name=False)['image']
|
||||
record.image_large = images['image_large']
|
||||
record.image_medium = images['image_medium']
|
||||
record.image_small = images['image_small']
|
||||
|
||||
@@ -572,7 +572,7 @@ class Partner(models.Model):
|
||||
("The selected company is not compatible with the companies of the related user(s)"))
|
||||
# no padding on the big image, because it's used as website logo
|
||||
tools.image_resize_images(vals, return_big=False)
|
||||
tools.image_resize_images(vals, return_medium=False, return_small=False, preserve_aspect_ratio=True)
|
||||
tools.image_resize_images(vals, return_medium=False, return_small=False)
|
||||
|
||||
result = True
|
||||
# To write in SUPERUSER on field is_company and avoid access rights problems.
|
||||
@@ -601,7 +601,7 @@ class Partner(models.Model):
|
||||
vals['image'] = self._get_default_image(vals.get('type'), vals.get('is_company'), vals.get('parent_id'))
|
||||
# no padding on the big image, because it's used as website logo
|
||||
tools.image_resize_images(vals, return_big=False)
|
||||
tools.image_resize_images(vals, return_medium=False, return_small=False, preserve_aspect_ratio=True)
|
||||
tools.image_resize_images(vals, return_medium=False, return_small=False)
|
||||
partners = super(Partner, self).create(vals_list)
|
||||
|
||||
if self.env.context.get('_partners_skip_fields_sync'):
|
||||
|
||||
@@ -20,7 +20,6 @@ class TestImage(TransactionCase):
|
||||
- image_resize_image_small
|
||||
- image_get_resized_images
|
||||
- image_resize_images
|
||||
- image_resize_and_sharpen
|
||||
- is_image_size_above
|
||||
"""
|
||||
def setUp(self):
|
||||
@@ -72,13 +71,13 @@ class TestImage(TransactionCase):
|
||||
res = tools.image_resize_image(self.base64_svg)
|
||||
self.assertEqual(res, self.base64_svg)
|
||||
|
||||
# CASE: ok with default parameters
|
||||
# CASE: ok with default parameters (keep ratio)
|
||||
image = tools.base64_to_image(tools.image_resize_image(self.base64_image_1920_1080))
|
||||
self.assertEqual(image.size, tools.IMAGE_BIG_SIZE)
|
||||
self.assertEqual(image.size, (1024, 576))
|
||||
|
||||
# CASE: correct resize
|
||||
# CASE: correct resize (keep ratio)
|
||||
image = tools.base64_to_image(tools.image_resize_image(self.base64_image_1920_1080, size=(800, 600)))
|
||||
self.assertEqual(image.size, (800, 600))
|
||||
self.assertEqual(image.size, (800, 450))
|
||||
|
||||
# CASE: change filetype to PNG
|
||||
image = tools.base64_to_image(tools.image_resize_image(self.base64_image_1920_1080, filetype='PNG'))
|
||||
@@ -105,41 +104,33 @@ class TestImage(TransactionCase):
|
||||
image = tools.base64_to_image(tools.image_resize_image(self.base64_image_1920_1080, size=(None, 108)))
|
||||
self.assertEqual(image.size, (192, 108))
|
||||
|
||||
# CASE: resize above original size
|
||||
image = tools.base64_to_image(tools.image_resize_image(self.base64_image_1920_1080, size=(3000, 2000)))
|
||||
self.assertEqual(image.size, (3000, 2000))
|
||||
|
||||
# CASE: avoid_if_small=True and size above, return original
|
||||
res = tools.image_resize_image(self.base64_image_1920_1080, size=(3000, 2000), avoid_if_small=True)
|
||||
# CASE: size above, return original
|
||||
res = tools.image_resize_image(self.base64_image_1920_1080, size=(3000, 2000))
|
||||
self.assertEqual(res, self.base64_image_1920_1080)
|
||||
|
||||
# CASE: same size, no sharpen
|
||||
# CASE: same size, no change
|
||||
res = tools.image_resize_image(self.base64_image_1920_1080, size=(1920, 1080))
|
||||
self.assertEqual(res, self.base64_image_1920_1080)
|
||||
|
||||
# CASE: upper_limit=True, no resize if above
|
||||
image = tools.base64_to_image(tools.image_resize_image(self.base64_image_1920_1080, size=(3000, 2000), upper_limit=True))
|
||||
self.assertEqual(image.size, (1920, 1080))
|
||||
# CASE: no resize if above
|
||||
res = tools.image_resize_image(self.base64_image_1920_1080, size=(3000, None))
|
||||
self.assertEqual(res, self.base64_image_1920_1080)
|
||||
|
||||
# CASE: upper_limit=True, keep ratio, no resize if above
|
||||
image = tools.base64_to_image(tools.image_resize_image(self.base64_image_1920_1080, size=(3000, None), upper_limit=True))
|
||||
self.assertEqual(image.size, (1920, 1080))
|
||||
# CASE: no resize if above
|
||||
res = tools.image_resize_image(self.base64_image_1920_1080, size=(None, 2000))
|
||||
self.assertEqual(res, self.base64_image_1920_1080)
|
||||
|
||||
# CASE: upper_limit=True, keep ratio, no resize if above
|
||||
image = tools.base64_to_image(tools.image_resize_image(self.base64_image_1920_1080, size=(None, 2000), upper_limit=True))
|
||||
self.assertEqual(image.size, (1920, 1080))
|
||||
|
||||
# CASE: upper_limit=True, vertical image, correct resize if below
|
||||
# CASE: vertical image, correct resize if below
|
||||
base64_image_1080_1920 = tools.image_to_base64(PIL.Image.new('RGB', (1080, 1920)), 'PNG')
|
||||
image = tools.base64_to_image(tools.image_resize_image(base64_image_1080_1920, size=(3000, 192), upper_limit=True))
|
||||
image = tools.base64_to_image(tools.image_resize_image(base64_image_1080_1920, size=(3000, 192)))
|
||||
self.assertEqual(image.size, (108, 192))
|
||||
|
||||
# CASE: preserve_aspect_ratio=True, adapt to width
|
||||
image = tools.base64_to_image(tools.image_resize_image(self.base64_image_1920_1080, size=(192, 200), preserve_aspect_ratio=True))
|
||||
# CASE: adapt to width
|
||||
image = tools.base64_to_image(tools.image_resize_image(self.base64_image_1920_1080, size=(192, 200)))
|
||||
self.assertEqual(image.size, (192, 108))
|
||||
|
||||
# CASE: preserve_aspect_ratio=True, adapt to height
|
||||
image = tools.base64_to_image(tools.image_resize_image(self.base64_image_1920_1080, size=(400, 108), preserve_aspect_ratio=True))
|
||||
# CASE: adapt to height
|
||||
image = tools.base64_to_image(tools.image_resize_image(self.base64_image_1920_1080, size=(400, 108)))
|
||||
self.assertEqual(image.size, (192, 108))
|
||||
|
||||
def test_11_image_optimize_for_web(self):
|
||||
@@ -257,12 +248,8 @@ class TestImage(TransactionCase):
|
||||
image = tools.base64_to_image(tools.limited_image_resize(self.base64_image_1920_1080, width=192, height=108, crop=True))
|
||||
self.assertEqual(image.size, (108, 108))
|
||||
|
||||
# CASE: if not upper_limit, max 500px * 500px
|
||||
# keep ratio
|
||||
image = tools.base64_to_image(tools.limited_image_resize(self.base64_image_1920_1080, width=1000, height=1000))
|
||||
self.assertEqual(image.size, (500, 500))
|
||||
|
||||
# CASE: if upper_limit=True, no limit
|
||||
image = tools.base64_to_image(tools.limited_image_resize(self.base64_image_1920_1080, width=1000, height=1000, upper_limit=True))
|
||||
self.assertEqual(image.size, (1000, 562))
|
||||
|
||||
def test_17_image_data_uri(self):
|
||||
|
||||
+28
-112
@@ -4,7 +4,6 @@ import base64
|
||||
import io
|
||||
|
||||
from PIL import Image
|
||||
from PIL import ImageEnhance
|
||||
from random import randrange
|
||||
|
||||
from odoo.tools.translate import _
|
||||
@@ -37,25 +36,10 @@ IMAGE_MAX_RESOLUTION = 45e6
|
||||
# Image resizing
|
||||
# ----------------------------------------
|
||||
|
||||
def image_resize_image(base64_source, size=IMAGE_BIG_SIZE, filetype=None, avoid_if_small=False, preserve_aspect_ratio=False, upper_limit=False):
|
||||
def image_resize_image(base64_source, size=IMAGE_BIG_SIZE, filetype=None):
|
||||
""" Function to resize an image. The image will be resized to the given
|
||||
size, while keeping the aspect ratios, and holes in the image will be
|
||||
filled with transparent background. The image will not be stretched if
|
||||
size, while keeping the aspect ratios. The image will not be resized if
|
||||
smaller than the expected size.
|
||||
Steps of the resizing:
|
||||
- Compute width and height if not specified.
|
||||
- if avoid_if_small: if both image sizes are smaller than the requested
|
||||
sizes, the original image is returned. This is used to avoid adding
|
||||
transparent content around images that we do not want to alter but
|
||||
just resize if too big. This is used for example when storing images
|
||||
in the 'image' field: we keep the original image, resized to a maximal
|
||||
size, without adding transparent content around it if smaller.
|
||||
- create a thumbnail of the source image through using the thumbnail
|
||||
function. Aspect ratios are preserved when using it. Note that if the
|
||||
source image is smaller than the expected size, it will not be
|
||||
extended, but filled to match the size.
|
||||
- create a transparent background that will hold the final image.
|
||||
- paste the thumbnail on the transparent background and center it.
|
||||
|
||||
:param base64_source: base64-encoded version of the source
|
||||
image; if False, returns False
|
||||
@@ -64,16 +48,15 @@ def image_resize_image(base64_source, size=IMAGE_BIG_SIZE, filetype=None, avoid_
|
||||
on height or width of the source image.
|
||||
:param filetype: the output filetype, by default the source image's
|
||||
:type filetype: str, any PIL image format (supported for creation)
|
||||
:param avoid_if_small: do not resize if image height and width
|
||||
are smaller than the expected size.
|
||||
"""
|
||||
if not base64_source:
|
||||
return False
|
||||
# Return unmodified content if no resize or we etect first 6 bits of '<'
|
||||
# (0x3C) for SVG documents - This will bypass XML files as well, but it's
|
||||
# harmless for these purposes
|
||||
if size == (None, None) or base64_source[:1] == b'P':
|
||||
if base64_source[:1] == b'P':
|
||||
return base64_source
|
||||
|
||||
image = base64_to_image(base64_source)
|
||||
# store filetype here, as Image.new below will lose image.format
|
||||
filetype = (filetype or image.format).upper()
|
||||
@@ -82,64 +65,19 @@ def image_resize_image(base64_source, size=IMAGE_BIG_SIZE, filetype=None, avoid_
|
||||
'BMP': 'PNG',
|
||||
}.get(filetype, filetype)
|
||||
|
||||
asked_width, asked_height = size
|
||||
if upper_limit:
|
||||
if asked_width:
|
||||
asked_width = min(asked_width, image.size[0])
|
||||
if asked_height:
|
||||
asked_height = min(asked_height, image.size[1])
|
||||
if size and (size[0] or size[1]):
|
||||
w, h = image.size
|
||||
asked_width = size[0] or (w * size[1]) // h
|
||||
asked_height = size[1] or (h * size[0]) // w
|
||||
|
||||
if image.size[0] >= image.size[1]:
|
||||
asked_height = None
|
||||
else:
|
||||
asked_width = None
|
||||
if asked_width is None and asked_height is None:
|
||||
return base64_source
|
||||
image.thumbnail((asked_width, asked_height), Image.LANCZOS)
|
||||
|
||||
if asked_width is None:
|
||||
asked_width = int(image.size[0] * (float(asked_height) / image.size[1]))
|
||||
if asked_height is None:
|
||||
asked_height = int(image.size[1] * (float(asked_width) / image.size[0]))
|
||||
size = asked_width, asked_height
|
||||
# check image size: do not create a thumbnail if avoiding smaller images
|
||||
if avoid_if_small and image.size[0] <= size[0] and image.size[1] <= size[1]:
|
||||
return base64_source
|
||||
|
||||
if image.size != size:
|
||||
image = image_resize_and_sharpen(image, size, preserve_aspect_ratio=preserve_aspect_ratio)
|
||||
if image.mode not in ["1", "L", "P", "RGB", "RGBA"] or (filetype == 'JPEG' and image.mode == 'RGBA'):
|
||||
image = image.convert("RGB")
|
||||
|
||||
return image_to_base64(image, filetype)
|
||||
|
||||
|
||||
def image_resize_and_sharpen(image, size, preserve_aspect_ratio=False, factor=2.0):
|
||||
"""
|
||||
Create a thumbnail by resizing while keeping ratio.
|
||||
A sharpen filter is applied for a better looking result.
|
||||
|
||||
:param image: PIL.Image.Image()
|
||||
:param size: 2-tuple(width, height)
|
||||
:param preserve_aspect_ratio: boolean (default: False)
|
||||
:param factor: Sharpen factor (default: 2.0)
|
||||
"""
|
||||
origin_mode = image.mode
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
image.thumbnail(size, Image.LANCZOS)
|
||||
if preserve_aspect_ratio:
|
||||
size = image.size
|
||||
sharpener = ImageEnhance.Sharpness(image)
|
||||
resized_image = sharpener.enhance(factor)
|
||||
# create a transparent image for background and paste the image on it
|
||||
image = Image.new('RGBA', size, (255, 255, 255, 0))
|
||||
image.paste(resized_image, ((size[0] - resized_image.size[0]) // 2, (size[1] - resized_image.size[1]) // 2))
|
||||
|
||||
if image.mode != origin_mode:
|
||||
image = image.convert(origin_mode)
|
||||
return image
|
||||
|
||||
|
||||
def image_optimize_for_web(base64_source, max_width=0, quality=80):
|
||||
"""Return the given `base64_source` optimized for web usage.
|
||||
|
||||
@@ -197,36 +135,36 @@ def image_optimize_for_web(base64_source, max_width=0, quality=80):
|
||||
return image_to_base64(image, **opt)
|
||||
|
||||
|
||||
def image_resize_image_big(base64_source, filetype=None, avoid_if_small=True, preserve_aspect_ratio=False, upper_limit=False):
|
||||
def image_resize_image_big(base64_source, filetype=None):
|
||||
""" Wrapper on image_resize_image, to resize images larger than the standard
|
||||
'big' image size: 1024x1024px.
|
||||
Refer to image_resize_image for the parameters.
|
||||
"""
|
||||
return image_resize_image(base64_source, IMAGE_BIG_SIZE, filetype, avoid_if_small, preserve_aspect_ratio, upper_limit)
|
||||
return image_resize_image(base64_source, IMAGE_BIG_SIZE, filetype)
|
||||
|
||||
|
||||
def image_resize_image_large(base64_source, filetype=None, avoid_if_small=True, preserve_aspect_ratio=False, upper_limit=False):
|
||||
def image_resize_image_large(base64_source, filetype=None):
|
||||
""" Wrapper on image_resize_image, to resize to the standard 'large'
|
||||
image size: 256x256.
|
||||
Refer to image_resize_image for the parameters.
|
||||
"""
|
||||
return image_resize_image(base64_source, IMAGE_LARGE_SIZE, filetype, avoid_if_small, preserve_aspect_ratio, upper_limit)
|
||||
return image_resize_image(base64_source, IMAGE_LARGE_SIZE, filetype)
|
||||
|
||||
|
||||
def image_resize_image_medium(base64_source, filetype=None, avoid_if_small=False, preserve_aspect_ratio=False, upper_limit=False):
|
||||
def image_resize_image_medium(base64_source, filetype=None):
|
||||
""" Wrapper on image_resize_image, to resize to the standard 'medium'
|
||||
image size: 128x128.
|
||||
Refer to image_resize_image for the parameters.
|
||||
"""
|
||||
return image_resize_image(base64_source, IMAGE_MEDIUM_SIZE, filetype, avoid_if_small, preserve_aspect_ratio, upper_limit)
|
||||
return image_resize_image(base64_source, IMAGE_MEDIUM_SIZE, filetype)
|
||||
|
||||
|
||||
def image_resize_image_small(base64_source, filetype=None, avoid_if_small=False, preserve_aspect_ratio=False, upper_limit=False):
|
||||
def image_resize_image_small(base64_source, filetype=None):
|
||||
""" Wrapper on image_resize_image, to resize to the standard 'small' image
|
||||
size: 64x64.
|
||||
Refer to image_resize_image for the parameters.
|
||||
"""
|
||||
return image_resize_image(base64_source, IMAGE_SMALL_SIZE, filetype, avoid_if_small, preserve_aspect_ratio, upper_limit)
|
||||
return image_resize_image(base64_source, IMAGE_SMALL_SIZE, filetype)
|
||||
|
||||
|
||||
# ----------------------------------------
|
||||
@@ -351,9 +289,7 @@ def is_image_size_above(base64_source, size=IMAGE_BIG_SIZE):
|
||||
|
||||
|
||||
def image_get_resized_images(base64_source,
|
||||
big_name='image', large_name='image_large', medium_name='image_medium', small_name='image_small',
|
||||
avoid_resize_big=True, avoid_resize_large=True, avoid_resize_medium=True, avoid_resize_small=True,
|
||||
preserve_aspect_ratio=False, upper_limit=False):
|
||||
big_name='image', large_name='image_large', medium_name='image_medium', small_name='image_small'):
|
||||
""" Standard tool function that returns a dictionary containing the
|
||||
big, medium, large and small versions of the source image.
|
||||
|
||||
@@ -368,20 +304,19 @@ def image_get_resized_images(base64_source,
|
||||
"""
|
||||
return_dict = dict()
|
||||
if big_name:
|
||||
return_dict[big_name] = image_resize_image_big(base64_source, avoid_if_small=avoid_resize_big, preserve_aspect_ratio=preserve_aspect_ratio, upper_limit=upper_limit)
|
||||
return_dict[big_name] = image_resize_image_big(base64_source)
|
||||
if large_name:
|
||||
return_dict[large_name] = image_resize_image_large(base64_source, avoid_if_small=avoid_resize_large, preserve_aspect_ratio=preserve_aspect_ratio, upper_limit=upper_limit)
|
||||
return_dict[large_name] = image_resize_image_large(base64_source)
|
||||
if medium_name:
|
||||
return_dict[medium_name] = image_resize_image_medium(base64_source, avoid_if_small=avoid_resize_medium, preserve_aspect_ratio=preserve_aspect_ratio, upper_limit=upper_limit)
|
||||
return_dict[medium_name] = image_resize_image_medium(base64_source)
|
||||
if small_name:
|
||||
return_dict[small_name] = image_resize_image_small(base64_source, avoid_if_small=avoid_resize_small, preserve_aspect_ratio=preserve_aspect_ratio, upper_limit=upper_limit)
|
||||
return_dict[small_name] = image_resize_image_small(base64_source)
|
||||
return return_dict
|
||||
|
||||
|
||||
def image_resize_images(vals,
|
||||
return_big=True, return_large=False, return_medium=True, return_small=True,
|
||||
big_name='image', large_name='image_large', medium_name='image_medium', small_name='image_small',
|
||||
preserve_aspect_ratio=False, upper_limit=False):
|
||||
big_name='image', large_name='image_large', medium_name='image_medium', small_name='image_small'):
|
||||
""" Update ``vals`` with image fields resized as expected. """
|
||||
big_image = vals.get(big_name)
|
||||
large_image = vals.get(large_name)
|
||||
@@ -392,11 +327,7 @@ def image_resize_images(vals,
|
||||
|
||||
if biggest_image:
|
||||
vals.update(image_get_resized_images(biggest_image,
|
||||
big_name=return_big and big_name, large_name=return_large and large_name, medium_name=return_medium and medium_name, small_name=return_small and small_name,
|
||||
avoid_resize_big=True, avoid_resize_large=True,
|
||||
avoid_resize_medium=(not big_image and not large_image),
|
||||
avoid_resize_small=(not big_image and not large_image and not medium_image),
|
||||
preserve_aspect_ratio=preserve_aspect_ratio, upper_limit=upper_limit))
|
||||
big_name=return_big and big_name, large_name=return_large and large_name, medium_name=return_medium and medium_name, small_name=return_small and small_name))
|
||||
elif any(f in vals for f in [big_name, large_name, medium_name, small_name]):
|
||||
if return_big:
|
||||
vals[big_name] = False
|
||||
@@ -408,7 +339,7 @@ def image_resize_images(vals,
|
||||
vals[small_name] = False
|
||||
|
||||
|
||||
def limited_image_resize(base64_source, width=None, height=None, crop=False, upper_limit=False, avoid_if_small=False):
|
||||
def limited_image_resize(base64_source, width=None, height=None, crop=False):
|
||||
"""Return the given `base64_source` image resized to the given `width` and
|
||||
`height`. Return the original image if both `width` and `height` are fasly.
|
||||
|
||||
@@ -422,32 +353,17 @@ def limited_image_resize(base64_source, width=None, height=None, crop=False, upp
|
||||
:type height: int
|
||||
|
||||
:param crop: True if the result should be cropped to a square.
|
||||
False to resize without cropping, then the result will be controlled
|
||||
by `upper_limit` and `avoid_if_small`
|
||||
False to resize without cropping.
|
||||
:type crop: bool
|
||||
|
||||
:param upper_limit: value sent to `image_resize_image` if `crop` is False
|
||||
:type upper_limit: bool
|
||||
|
||||
:param avoid_if_small: value sent to `image_resize_image` if `crop` is False
|
||||
:type avoid_if_small: bool
|
||||
|
||||
:return: resulting image base64 encoded
|
||||
:rtype: string or bytes
|
||||
"""
|
||||
if base64_source and (width or height):
|
||||
height = int(height or 0)
|
||||
width = int(width or 0)
|
||||
if crop:
|
||||
return crop_image(base64_source, type='center', size=(width or None, height or None), ratio=(1, 1))
|
||||
return crop_image(base64_source, type='center', size=(width, height), ratio=(1, 1))
|
||||
else:
|
||||
if not upper_limit:
|
||||
# resize maximum 500*500
|
||||
width = min(width, 500)
|
||||
height = min(height, 500)
|
||||
return image_resize_image(
|
||||
base64_source=base64_source, size=(width or None, height or None), upper_limit=upper_limit,
|
||||
avoid_if_small=avoid_if_small)
|
||||
return image_resize_image(base64_source=base64_source, size=(width, height))
|
||||
return base64_source
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user