[IMP] base, tools, web_editor, *: use original image if size increases

*: web_tour, website

When no transformation is applied on an image, changing the quality
sometimes increases its storage size.

This commit makes sure that the original image remains used if only the
image quality is modified and if this makes its storage size bigger.

Fixes #61619
task-2835144

closes odoo/odoo#103398

Signed-off-by: Romain Derie (rde) <rde@odoo.com>
This commit is contained in:
Benoit Socias
2023-07-19 13:12:51 +02:00
parent c512f098a7
commit 567e5b58d5
6 changed files with 104 additions and 3 deletions
@@ -334,7 +334,13 @@ async function applyModifications(img, dataOptions = {}) {
ctx.fillRect(0, 0, result.width, result.height);
// Quality
return result.toDataURL(mimetype, quality / 100);
const dataURL = result.toDataURL(mimetype, quality / 100);
const newSize = getDataURLBinarySize(dataURL);
const originalSize = _getImageSizeFromCache(originalSrc);
const isChanged = !!perspective || !!glFilter ||
original.width !== result.width || original.height !== result.height ||
original.width !== croppedImg.width || original.height !== croppedImg.height;
return (isChanged || originalSize >= newSize) ? dataURL : await _loadImageDataURL(originalSrc);
}
/**
@@ -391,9 +397,18 @@ async function _updateImageData(src, key = 'objectURL') {
} else {
value = URL.createObjectURL(blob);
}
imageCache.set(src, Object.assign(currentImageData || {}, {[key]: value}));
imageCache.set(src, Object.assign(currentImageData || {}, {[key]: value, size: blob.size}));
return value;
}
/**
* Returns the size of a cached image.
*
* @param {String} src used as a key on the image cache map.
* @returns {Number} size of the image in bytes.
*/
function _getImageSizeFromCache(src) {
return imageCache.get(src).size;
}
/**
* Activates the cropper on a given image.
*
@@ -211,6 +211,9 @@ export class RunningTourActionHelper {
text_blur(text, element) {
this._text_blur(this._get_action_values(element), text);
}
range(text, element) {
this._range(this._get_action_values(element), text);
}
drag_and_drop(to, element) {
this._drag_and_drop_jquery(this._get_action_values(element), to);
}
@@ -313,6 +316,10 @@ export class RunningTourActionHelper {
values.$element.trigger("focusout");
values.$element.trigger("blur");
}
_range(values, text) {
values.$element[0].value = text;
values.$element[0].dispatchEvent(new Event('change', { bubbles: true, cancelable: false }));
}
_calculateCenter($el, selector) {
const center = $el.offset();
if (selector && selector.indexOf("iframe") !== -1) {
@@ -0,0 +1,55 @@
/** @odoo-module */
import wTourUtils from 'website.tour_utils';
wTourUtils.registerWebsitePreviewTour('website_image_quality', {
test: true,
url: '/',
edition: true,
}, [
wTourUtils.dragNDrop({
id: 's_text_image',
name: 'Text - Image',
}),
{
content: "Select image",
trigger: 'iframe .s_text_image img',
},
{
content: "Set low quality",
trigger: 'we-customizeblock-options:has(we-title:contains("Image")) we-range[data-set-quality] input',
run: 'range 5',
},
{
content: "Wait for image update: NOT original image",
trigger: 'iframe .s_text_image img:not([src$="s_text_image_default_image"])',
run: () => {}, // It is a check.
},
{
content: "Check image size",
// Reached size cannot be hardcoded because it changes with
// different versions of Chrome.
trigger: 'we-customizeblock-options:has(we-title:contains("Image")) .o_we_image_weight:contains(" kb"):not(:contains("16.1 kb"))',
run() {
// Make sure the reached size is smaller than the original one.
if (parseFloat(this.$anchor[0].innerText) >= 47.5) {
console.error("Image size should be smaller than original");
}
},
},
{
content: "Set high quality",
trigger: 'we-customizeblock-options:has(we-title:contains("Image")) we-range[data-set-quality] input',
run: 'range 99',
},
{
content: "Wait for image update: back to original image",
trigger: 'iframe .s_text_image img[src$="QUntVvdqEdljipEUu7ez5lb6IqyGKzQmRxVnZX/2Q=="]',
run: () => {}, // It is a check.
},
{
content: "Check image size",
trigger: 'we-customizeblock-options:has(we-title:contains("Image")) .o_we_image_weight:contains("47.5 kb")',
run: () => {}, // It is a check.
},
]);
+6
View File
@@ -1,3 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from urllib.parse import urlparse
import odoo.tests
@@ -51,3 +54,6 @@ class TestWebsiteAttachment(odoo.tests.HttpCase):
req = self.opener.get(base + '/web/image/test.an_image_redirect_301', allow_redirects=True)
self.assertEqual(req.status_code, 200)
def test_02_image_quality(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'website_image_quality', login="admin")
+13
View File
@@ -177,6 +177,19 @@ class TestImage(TransactionCase):
res = tools.image_process(self.img_1920x1080_jpeg)
self.assertLessEqual(len(res), len(self.img_1920x1080_jpeg))
# CASE: JPEG optimize + bigger size => original
pil_image = Image.new('RGB', (1920, 1080), color=self.bg_color)
# Drawing non trivial content so that optimization matters.
ImageDraw.Draw(pil_image).ellipse(xy=[
(400, 0),
(1500, 1080)
], fill=self.fill_color, outline=(240, 25, 40), width=10)
image = tools.image_apply_opt(pil_image, 'JPEG')
res = tools.image_process(image, quality=50)
self.assertLess(len(res), len(image), "Low quality image should be smaller than original")
res = tools.image_process(image, quality=99)
self.assertEqual(len(res), len(image), "Original should be returned if size increased")
# CASE: GIF doesn't apply quality, just optimize
image = tools.image_apply_opt(Image.new('RGB', (1080, 1920)), 'GIF')
res = tools.image_process(image)
+6 -1
View File
@@ -151,7 +151,12 @@ class ImageProcess():
if output_image.mode not in ["1", "L", "P", "RGB", "RGBA"] or (output_format == 'JPEG' and output_image.mode == 'RGBA'):
output_image = output_image.convert("RGB")
return image_apply_opt(output_image, **opt)
output_bytes = image_apply_opt(output_image, **opt)
if len(output_bytes) >= len(self.source) and self.original_format == output_format and not self.operationsCount:
# Format has not changed and image content is unchanged but the
# reached binary is bigger: rather use the original.
return self.source
return output_bytes
def resize(self, max_width=0, max_height=0):
"""Resize the image.