Skip to content
Permalink
Browse files

[IMP] tools,base: keep image ratio on resize

Before
======

Since commit: 2e3848b

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.

PR: #31811
  • Loading branch information...
seb-odoo committed Mar 18, 2019
1 parent 5a1fda8 commit d7aff8be327f2c45851f37d3952f9bbd5a80b3d7
@@ -1039,8 +1039,7 @@ def content_common(self, xmlid=None, model='ir.attachment', id=None, field='data
'/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, **kw):
download=None, width=0, height=0, crop=False, access_token=None, **kw):
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,
@@ -1057,7 +1056,7 @@ def content_image(self, xmlid=None, model='ir.attachment', id=None, field='datas
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 @@ def record_to_html(self, record, field_name, options):

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 @@ def record_to_html(self, record, field_name, options):
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']])
@@ -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",
@@ -76,7 +76,7 @@ def _prepare_user_profile_values(self, user, **post):
@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()

@@ -98,7 +98,7 @@ def get_user_profile_avatar(self, user_id, field='image_large', width=0, height=
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)))
@@ -156,29 +156,29 @@ def test_01_admin_shop_zoom_tour(self):
image = Image.open(io.BytesIO(base64.b64decode(product_green.image)))
self.assertEqual(image.size, (1024, 576))

# 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))
image = Image.open(io.BytesIO(base64.b64decode(product_red.image_large)))
self.assertEqual(image.size, (256, 256))
self.assertEqual(image.size, (256, 160))
image = Image.open(io.BytesIO(base64.b64decode(product_green.image_large)))
self.assertEqual(image.size, (256, 256))
self.assertEqual(image.size, (256, 144))

# 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))
image = Image.open(io.BytesIO(base64.b64decode(product_red.image_medium)))
self.assertEqual(image.size, (128, 128))
self.assertEqual(image.size, (128, 80))
image = Image.open(io.BytesIO(base64.b64decode(product_green.image_medium)))
self.assertEqual(image.size, (128, 128))
self.assertEqual(image.size, (128, 72))

# 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))
image = Image.open(io.BytesIO(base64.b64decode(product_red.image_small)))
self.assertEqual(image.size, (64, 64))
self.assertEqual(image.size, (64, 40))
image = Image.open(io.BytesIO(base64.b64decode(product_green.image_small)))
self.assertEqual(image.size, (64, 64))
self.assertEqual(image.size, (64, 36))

# self.env.cr.commit() # uncomment to save the product to test in browser

@@ -500,7 +500,7 @@ def slide_get_pdf_content(self, slide):
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()
@@ -521,7 +521,7 @@ def slide_get_image(self, slide_id, field='image_medium', width=0, height=0, cro
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)))
@@ -35,7 +35,7 @@ def _compute_image_raw(self):
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']
@@ -529,7 +529,7 @@ def write(self, vals):
("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.
@@ -556,7 +556,7 @@ def create(self, vals_list):
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)
for partner, vals in zip(partners, vals_list):
partner._fields_sync(vals)
@@ -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 @@ def test_10_image_resize_image(self):
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 @@ def test_10_image_resize_image(self):
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: 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=(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=(None, 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=(None, 2000))
self.assertEqual(res, self.base64_image_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):
@@ -263,12 +254,8 @@ def test_16_limited_image_resize(self):
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):
Oops, something went wrong.

0 comments on commit d7aff8b

Please sign in to comment.
You can’t perform that action at this time.