Skip to content

Commit 60e25f7

Browse files
committed
[FIX] web_editor, website, *: avoid implicit conversions when possible
*: website_event This commit deals with implicit conversion when converting to webp isn't available: - When trying to generate alternative webp attachments, don't - In website, suggest jpeg alternatives instead of webp. When the current image's format isn't available on this browser, display it in the options but don't allow selecting it. When applying a modification, use its same-size counterpart (webp <=> jpeg) instead. task-3847470
1 parent 55f5bf0 commit 60e25f7

File tree

6 files changed

+188
-89
lines changed

6 files changed

+188
-89
lines changed

addons/web_editor/static/src/js/editor/snippets.options.js

Lines changed: 140 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
getDataURLBinarySize,
3333
} from "@web_editor/js/editor/image_processing";
3434
import * as OdooEditorLib from "@web_editor/js/editor/odoo-editor/src/OdooEditor";
35+
import { canExportCanvasAsWebp } from "@web/core/utils/image_processing";
3536
import { pick } from "@web/core/utils/objects";
3637
import { _t } from "@web/core/l10n/translation";
3738
import {
@@ -6205,18 +6206,11 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
62056206
/**
62066207
* @see this.selectClass for parameters
62076208
*/
6208-
selectFormat(previewMode, widgetValue, params) {
6209-
const values = widgetValue.split(' ');
6209+
async selectFormat(previewMode, widgetValue, params) {
6210+
const [resizeWidth, mimetype] = widgetValue.split(" ");
62106211
const image = this._getImg();
6211-
image.dataset.resizeWidth = values[0];
6212-
if (image.dataset.shape) {
6213-
// If the image has a shape, modify its originalMimetype attribute.
6214-
image.dataset.originalMimetype = values[1];
6215-
} else {
6216-
// If the image does not have a shape, modify its mimetype
6217-
// attribute.
6218-
image.dataset.mimetype = values[1];
6219-
}
6212+
image.dataset.resizeWidth = resizeWidth;
6213+
this._setImageMimetype(image, mimetype);
62206214
return this._applyOptions();
62216215
},
62226216
/**
@@ -6285,17 +6279,8 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
62856279
});
62866280

62876281
switch (methodName) {
6288-
case "selectFormat": {
6289-
const currentImageMimetype = this._getImageMimetype(img);
6290-
const availableFormats = (await this._computeAvailableFormats()).map(
6291-
([value, [, targetFormat]]) => `${Math.round(value)} ${targetFormat}`
6292-
);
6293-
const currentFormatSize = img.naturalWidth.toString();
6294-
const currentFormat = `${currentFormatSize} ${currentImageMimetype}`;
6295-
return availableFormats.includes(currentFormat)
6296-
? currentFormat
6297-
: availableFormats.find((format) => format.startsWith(currentFormatSize));
6298-
}
6282+
case 'selectFormat':
6283+
return this._getCurrentFormat(img);
62996284
case 'setFilter':
63006285
return img.dataset.filter;
63016286
case 'glFilter':
@@ -6324,9 +6309,16 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
63246309
return;
63256310
}
63266311
const $select = $(uiFragment).find('we-select[data-name=format_select_opt]');
6327-
(await this._computeAvailableFormats()).forEach(([value, [label, targetFormat]]) => {
6328-
$select.append(`<we-button data-select-format="${Math.round(value)} ${targetFormat}" class="o_we_badge_at_end">${label} <span class="badge rounded-pill text-bg-dark">${targetFormat.split('/')[1]}</span></we-button>`);
6329-
});
6312+
(await this._computeAvailableFormats()).forEach(
6313+
([value, [label, targetFormat, isDisabled]]) => {
6314+
const selectFormat = `${Math.round(value)} ${targetFormat}`;
6315+
const unavailableOptionDependencies = isDisabled ? ' data-dependencies="fake"' : "";
6316+
const mimetypeBadge = targetFormat.split("/")[1];
6317+
$select.append(
6318+
`<we-button data-select-format="${selectFormat}" class="o_we_badge_at_end"${unavailableOptionDependencies}>${label} <span class="badge rounded-pill text-bg-dark">${mimetypeBadge}</span></we-button>`
6319+
);
6320+
}
6321+
);
63306322

63316323
if (!['image/jpeg', 'image/webp'].includes(this._getImageMimetype(img))) {
63326324
const optQuality = uiFragment.querySelector('we-range[data-set-quality]');
@@ -6346,28 +6338,53 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
63466338
return [];
63476339
}
63486340
const img = this._getImg();
6349-
const original = await loadImage(this.originalSrc);
6350-
const maxWidth = img.dataset.width ? img.naturalWidth : original.naturalWidth;
6351-
const optimizedWidth = Math.min(maxWidth, this._computeMaxDisplayWidth());
6341+
const originalSize = await this._getOriginalSize();
6342+
const optimizedWidth = Math.min(originalSize, this._computeMaxDisplayWidth());
63526343
this.optimizedWidth = optimizedWidth;
6353-
const widths = {
6354-
128: ['128px', 'image/webp'],
6355-
256: ['256px', 'image/webp'],
6356-
512: ['512px', 'image/webp'],
6357-
1024: ['1024px', 'image/webp'],
6358-
1920: ['1920px', 'image/webp'],
6344+
const optimizedMimetype = canExportCanvasAsWebp() ? "image/webp" : "image/jpeg";
6345+
const formatsByWidth = {
6346+
128: ["128px", optimizedMimetype],
6347+
256: ["256px", optimizedMimetype],
6348+
512: ["512px", optimizedMimetype],
6349+
1024: ["1024px", optimizedMimetype],
6350+
1920: ["1920px", optimizedMimetype],
63596351
};
6360-
widths[img.naturalWidth] = [_t("%spx", img.naturalWidth), 'image/webp'];
6361-
widths[optimizedWidth] = [_t("%spx (Suggested)", optimizedWidth), 'image/webp'];
6352+
formatsByWidth[img.naturalWidth] = [_t("%spx", img.naturalWidth), optimizedMimetype];
6353+
formatsByWidth[optimizedWidth] = [
6354+
_t("%spx (Suggested)", optimizedWidth),
6355+
optimizedMimetype,
6356+
];
63626357
const mimetypeBeforeConversion = img.dataset.mimetypeBeforeConversion;
6363-
widths[maxWidth] = [_t("%spx (Original)", maxWidth), mimetypeBeforeConversion];
6364-
if (mimetypeBeforeConversion !== "image/webp") {
6365-
// Avoid a key collision by subtracting 0.1 - putting the webp
6366-
// above the original format one of the same size.
6367-
widths[maxWidth - 0.1] = [_t("%spx", maxWidth), 'image/webp'];
6368-
}
6369-
return Object.entries(widths)
6370-
.filter(([width]) => width <= maxWidth)
6358+
formatsByWidth[originalSize] = [
6359+
_t("%spx (Original)", originalSize),
6360+
mimetypeBeforeConversion,
6361+
];
6362+
if (mimetypeBeforeConversion !== optimizedMimetype) {
6363+
// Avoid key collision and ensure the optimized format is above the
6364+
// original one of the same size.
6365+
formatsByWidth[originalSize - 0.1] = [_t("%spx", originalSize), optimizedMimetype];
6366+
}
6367+
6368+
// If the currently selected format is unavailable, add it as an
6369+
// unselectable option.
6370+
// This handles cases such as:
6371+
// - Switching between a browser where webp is available and one where
6372+
// webp is not
6373+
// - Changing the specs of the available formats, where some previously
6374+
// used format cannot be selected anymore
6375+
// - External modification of the format
6376+
const currentFormat = this._getCurrentFormat(img);
6377+
const [selectedSize, selectedMimetype] = currentFormat?.split(" ") || [];
6378+
if (
6379+
selectedSize &&
6380+
selectedMimetype &&
6381+
(!formatsByWidth[selectedSize] || formatsByWidth[selectedSize][1] !== selectedMimetype)
6382+
) {
6383+
formatsByWidth[selectedSize - 0.2] = [_t("%spx", selectedSize), selectedMimetype, true];
6384+
}
6385+
6386+
return Object.entries(formatsByWidth)
6387+
.filter(([width]) => width <= originalSize)
63716388
.sort(([v1], [v2]) => v1 - v2);
63726389
},
63736390
/**
@@ -6394,9 +6411,12 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
63946411
delete img.dataset.mimetype;
63956412
return;
63966413
}
6414+
const targetMimetype = await this._getImageTargetMimetype();
63976415
const { dataURL, mimetype } = await applyModifications(
63986416
img,
6399-
{ mimetype: this._getImageMimetype(img) },
6417+
{
6418+
mimetype: targetMimetype,
6419+
},
64006420
true // TODO: remove in master
64016421
);
64026422
this._filesize = getDataURLBinarySize(dataURL) / 1024;
@@ -6438,12 +6458,12 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
64386458
await this._loadImageInfo();
64396459
await this._rerenderXML();
64406460
const img = this._getImg();
6441-
if (!['image/gif', 'image/svg+xml'].includes(img.dataset.mimetype)) {
6461+
if (
6462+
!["image/gif", "image/svg+xml"].includes(img.dataset.mimetype) ||
6463+
(img.dataset.shape && img.dataset.originalMimetype !== "image/gif")
6464+
) {
64426465
// Convert to recommended format and width.
6443-
img.dataset.mimetype = 'image/webp';
6444-
img.dataset.resizeWidth = this.optimizedWidth;
6445-
} else if (img.dataset.shape && img.dataset.originalMimetype !== "image/gif") {
6446-
img.dataset.originalMimetype = "image/webp";
6466+
this._setImageMimetype(img, canExportCanvasAsWebp() ? "image/webp" : "image/jpeg");
64476467
img.dataset.resizeWidth = this.optimizedWidth;
64486468
}
64496469
await this._applyOptions();
@@ -6530,6 +6550,70 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
65306550
|| params.optionsPossibleValues.setQuality
65316551
|| widgetName === 'format_select_opt';
65326552
},
6553+
/**
6554+
* @param {HTMLImageElement} [image] Override image element.
6555+
* @return {string} The image's current width.
6556+
* @private
6557+
*/
6558+
_getImageWidth(image = this._getImg()) {
6559+
return Math.trunc(
6560+
image.dataset.resizeWidth || image.dataset.width || image.naturalWidth
6561+
).toString();
6562+
},
6563+
/**
6564+
* @return {Promise<number>} The image's original size.
6565+
* @private
6566+
*/
6567+
async _getOriginalSize() {
6568+
const image = this._getImg();
6569+
const originalImage = await loadImage(this.originalSrc);
6570+
return image.dataset.width ? image.naturalWidth : originalImage.naturalWidth;
6571+
},
6572+
/**
6573+
* @param {HTMLImageElement} img
6574+
* @return {string} The image's current format (widget value).
6575+
* @private
6576+
*/
6577+
_getCurrentFormat(img) {
6578+
return `${img.naturalWidth} ${this._getImageMimetype(img)}`;
6579+
},
6580+
/**
6581+
* Returns whether the format is the image's original format.
6582+
*
6583+
* @param {string} size
6584+
* @param {string} mimetype
6585+
* @param {HTMLImageElement} [image] Override image element.
6586+
* @return {Promise<boolean>}
6587+
* @private
6588+
*/
6589+
async _isOriginalFormat(size, mimetype, image = this._getImg()) {
6590+
const originalMimetype = image.dataset.mimetypeBeforeConversion;
6591+
if (mimetype !== originalMimetype) {
6592+
return false;
6593+
}
6594+
6595+
const originalSize = await this._getOriginalSize();
6596+
return size.toString() === originalSize.toString();
6597+
},
6598+
/**
6599+
* Get the appropriate mimetype to applyModifications.
6600+
* Avoid implicit conversions.
6601+
* Only target available formats.
6602+
*
6603+
* @param {HTMLImageElement} [image] Override image element.
6604+
* @private
6605+
*/
6606+
async _getImageTargetMimetype(image = this._getImg()) {
6607+
const currentSize = this._getImageWidth(image);
6608+
const currentMimetype = this._getImageMimetype(image);
6609+
6610+
const isOriginalFormat = await this._isOriginalFormat(currentSize, currentMimetype);
6611+
if (!isOriginalFormat && ["image/webp", "image/jpeg"].includes(currentMimetype)) {
6612+
return canExportCanvasAsWebp() ? "image/webp" : "image/jpeg";
6613+
} else {
6614+
return currentMimetype;
6615+
}
6616+
},
65336617
});
65346618

65356619
/**
@@ -6614,7 +6698,7 @@ registry.ImageTools = ImageHandlerOption.extend({
66146698
await new Promise(resolve => {
66156699
this.$target.one('image_cropper_destroyed', async () => {
66166700
if (isGif(this._getImageMimetype(img))) {
6617-
img.dataset[img.dataset.shape ? 'originalMimetype' : 'mimetype'] = 'image/png';
6701+
this._setImageMimetype(img, "image/png");
66186702
}
66196703
await this._reapplyCurrentShape();
66206704
resolve();
@@ -6678,6 +6762,7 @@ registry.ImageTools = ImageHandlerOption.extend({
66786762
// temporarily into the body.
66796763
const imageCropWrapperElement = document.createElement('div');
66806764
document.body.append(imageCropWrapperElement);
6765+
img.dataset.targetMimetype = img.dataset.mimetypeBeforeConversion;
66816766
const imageCropWrapper = await attachComponent(this, imageCropWrapperElement, ImageCrop, {
66826767
rpc: this.rpc,
66836768
activeOnStart: true,
@@ -6745,10 +6830,11 @@ registry.ImageTools = ImageHandlerOption.extend({
67456830
}
67466831
} else {
67476832
// Re-applying the modifications and deleting the shapes
6833+
const targetMimetype = await this._getImageTargetMimetype();
67486834
const { dataURL, mimetype } = await applyModifications(
67496835
img,
67506836
{
6751-
mimetype: this._getImageMimetype(img),
6837+
mimetype: targetMimetype,
67526838
},
67536839
true // TODO: remove in master
67546840
);
@@ -7124,10 +7210,11 @@ registry.ImageTools = ImageHandlerOption.extend({
71247210
// We will store the image in base64 inside the SVG.
71257211
// applyModifications will return a dataURL with the current filters
71267212
// and size options.
7213+
const targetMimetype = await this._getImageTargetMimetype(img);
71277214
const { dataURL: imgDataURL, mimetype } = await applyModifications(
71287215
img,
71297216
{
7130-
mimetype: this._getImageMimetype(img),
7217+
mimetype: targetMimetype,
71317218
perspective: svg.dataset.imgPerspective || null,
71327219
imgAspectRatio: svg.dataset.imgAspectRatio || null,
71337220
svgAspectRatio: svgAspectRatio,

addons/web_editor/static/src/js/wysiwyg/wysiwyg.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import weUtils from "@web_editor/js/common/utils";
2020
import { isSelectionInSelectors, peek } from '@web_editor/js/editor/odoo-editor/src/utils/utils';
2121
import { PeerToPeer, RequestError } from "@web_editor/js/wysiwyg/PeerToPeer";
2222
import { uniqueId } from "@web/core/utils/functions";
23-
import { convertCanvasToDataURL } from "@web/core/utils/image_processing";
23+
import { canExportCanvasAsWebp, convertCanvasToDataURL } from "@web/core/utils/image_processing";
2424
import { groupBy } from "@web/core/utils/arrays";
2525
import { debounce } from "@web/core/utils/timing";
2626
import { registry } from "@web/core/registry";
@@ -3526,13 +3526,15 @@ export class Wysiwyg extends Component {
35263526

35273527
const generateAltData = (mimetype) => {
35283528
const imageData = convertCanvasToDataURL(canvas, mimetype, 0.75);
3529-
if (!altData[size]) {
3530-
altData[size] = {};
3529+
if (imageData.mimetype === mimetype) {
3530+
if (!altData[size]) {
3531+
altData[size] = {};
3532+
}
3533+
altData[size][imageData.mimetype] = imageData.base64Part;
35313534
}
3532-
altData[size][imageData.mimetype] = imageData.base64Part;
35333535
};
35343536
generateAltData("image/jpeg");
3535-
if (size !== originalSize) {
3537+
if (size !== originalSize && canExportCanvasAsWebp()) {
35363538
generateAltData("image/webp");
35373539
}
35383540
}

addons/website/static/src/js/editor/snippets.options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { loadCSS } from "@web/core/assets";
44
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
55
import { Dialog } from "@web/core/dialog/dialog";
66
import { useChildRef } from "@web/core/utils/hooks";
7+
import { canExportCanvasAsWebp } from "@web/core/utils/image_processing";
78
import weUtils from "@web_editor/js/common/utils";
89
import options from "@web_editor/js/editor/snippets.options";
910
import { NavbarLinkPopoverWidget } from "@website/js/widgets/link_popover_widget";
@@ -2991,7 +2992,7 @@ options.registry.CoverProperties = options.Class.extend({
29912992
"image/gif",
29922993
"image/svg+xml",
29932994
"image/webp",
2994-
].includes(originalMimetype)) {
2995+
].includes(originalMimetype) && canExportCanvasAsWebp()) {
29952996
// Convert to webp but keep original width.
29962997
const { dataURL, mimetype } = await applyModifications(
29972998
imgEl,

addons/website/static/src/snippets/s_image_gallery/options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/** @odoo-module **/
22

3+
import { canExportCanvasAsWebp } from "@web/core/utils/image_processing";
34
import { MediaDialog } from "@web_editor/components/media_dialog/media_dialog";
45
import options from "@web_editor/js/editor/snippets.options";
56
import wUtils from '@website/js/utils';
@@ -526,7 +527,7 @@ options.registry.GalleryImageList = options.registry.GalleryLayout.extend({
526527
"image/gif",
527528
"image/svg+xml",
528529
"image/webp",
529-
].includes(originalMimetype)) {
530+
].includes(originalMimetype) && canExportCanvasAsWebp()) {
530531
// Convert to webp but keep original width.
531532
applyModifications(
532533
imgEl,

0 commit comments

Comments
 (0)