@@ -32,6 +32,7 @@ import {
32
32
getDataURLBinarySize ,
33
33
} from "@web_editor/js/editor/image_processing" ;
34
34
import * as OdooEditorLib from "@web_editor/js/editor/odoo-editor/src/OdooEditor" ;
35
+ import { canExportCanvasAsWebp } from "@web/core/utils/image_processing" ;
35
36
import { pick } from "@web/core/utils/objects" ;
36
37
import { _t } from "@web/core/l10n/translation" ;
37
38
import {
@@ -6205,18 +6206,11 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
6205
6206
/**
6206
6207
* @see this.selectClass for parameters
6207
6208
*/
6208
- selectFormat ( previewMode , widgetValue , params ) {
6209
- const values = widgetValue . split ( ' ' ) ;
6209
+ async selectFormat ( previewMode , widgetValue , params ) {
6210
+ const [ resizeWidth , mimetype ] = widgetValue . split ( " " ) ;
6210
6211
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 ) ;
6220
6214
return this . _applyOptions ( ) ;
6221
6215
} ,
6222
6216
/**
@@ -6285,17 +6279,8 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
6285
6279
} ) ;
6286
6280
6287
6281
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 ) ;
6299
6284
case 'setFilter' :
6300
6285
return img . dataset . filter ;
6301
6286
case 'glFilter' :
@@ -6324,9 +6309,16 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
6324
6309
return ;
6325
6310
}
6326
6311
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
+ ) ;
6330
6322
6331
6323
if ( ! [ 'image/jpeg' , 'image/webp' ] . includes ( this . _getImageMimetype ( img ) ) ) {
6332
6324
const optQuality = uiFragment . querySelector ( 'we-range[data-set-quality]' ) ;
@@ -6346,28 +6338,53 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
6346
6338
return [ ] ;
6347
6339
}
6348
6340
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 ( ) ) ;
6352
6343
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 ] ,
6359
6351
} ;
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
+ ] ;
6362
6357
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 )
6371
6388
. sort ( ( [ v1 ] , [ v2 ] ) => v1 - v2 ) ;
6372
6389
} ,
6373
6390
/**
@@ -6394,9 +6411,12 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
6394
6411
delete img . dataset . mimetype ;
6395
6412
return ;
6396
6413
}
6414
+ const targetMimetype = await this . _getImageTargetMimetype ( ) ;
6397
6415
const { dataURL, mimetype } = await applyModifications (
6398
6416
img ,
6399
- { mimetype : this . _getImageMimetype ( img ) } ,
6417
+ {
6418
+ mimetype : targetMimetype ,
6419
+ } ,
6400
6420
true // TODO: remove in master
6401
6421
) ;
6402
6422
this . _filesize = getDataURLBinarySize ( dataURL ) / 1024 ;
@@ -6438,12 +6458,12 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
6438
6458
await this . _loadImageInfo ( ) ;
6439
6459
await this . _rerenderXML ( ) ;
6440
6460
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
+ ) {
6442
6465
// 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" ) ;
6447
6467
img . dataset . resizeWidth = this . optimizedWidth ;
6448
6468
}
6449
6469
await this . _applyOptions ( ) ;
@@ -6530,6 +6550,70 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
6530
6550
|| params . optionsPossibleValues . setQuality
6531
6551
|| widgetName === 'format_select_opt' ;
6532
6552
} ,
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
+ } ,
6533
6617
} ) ;
6534
6618
6535
6619
/**
@@ -6614,7 +6698,7 @@ registry.ImageTools = ImageHandlerOption.extend({
6614
6698
await new Promise ( resolve => {
6615
6699
this . $target . one ( 'image_cropper_destroyed' , async ( ) => {
6616
6700
if ( isGif ( this . _getImageMimetype ( img ) ) ) {
6617
- img . dataset [ img . dataset . shape ? 'originalMimetype' : 'mimetype' ] = ' image/png' ;
6701
+ this . _setImageMimetype ( img , " image/png" ) ;
6618
6702
}
6619
6703
await this . _reapplyCurrentShape ( ) ;
6620
6704
resolve ( ) ;
@@ -6678,6 +6762,7 @@ registry.ImageTools = ImageHandlerOption.extend({
6678
6762
// temporarily into the body.
6679
6763
const imageCropWrapperElement = document . createElement ( 'div' ) ;
6680
6764
document . body . append ( imageCropWrapperElement ) ;
6765
+ img . dataset . targetMimetype = img . dataset . mimetypeBeforeConversion ;
6681
6766
const imageCropWrapper = await attachComponent ( this , imageCropWrapperElement , ImageCrop , {
6682
6767
rpc : this . rpc ,
6683
6768
activeOnStart : true ,
@@ -6745,10 +6830,11 @@ registry.ImageTools = ImageHandlerOption.extend({
6745
6830
}
6746
6831
} else {
6747
6832
// Re-applying the modifications and deleting the shapes
6833
+ const targetMimetype = await this . _getImageTargetMimetype ( ) ;
6748
6834
const { dataURL, mimetype } = await applyModifications (
6749
6835
img ,
6750
6836
{
6751
- mimetype : this . _getImageMimetype ( img ) ,
6837
+ mimetype : targetMimetype ,
6752
6838
} ,
6753
6839
true // TODO: remove in master
6754
6840
) ;
@@ -7124,10 +7210,11 @@ registry.ImageTools = ImageHandlerOption.extend({
7124
7210
// We will store the image in base64 inside the SVG.
7125
7211
// applyModifications will return a dataURL with the current filters
7126
7212
// and size options.
7213
+ const targetMimetype = await this . _getImageTargetMimetype ( img ) ;
7127
7214
const { dataURL : imgDataURL , mimetype } = await applyModifications (
7128
7215
img ,
7129
7216
{
7130
- mimetype : this . _getImageMimetype ( img ) ,
7217
+ mimetype : targetMimetype ,
7131
7218
perspective : svg . dataset . imgPerspective || null ,
7132
7219
imgAspectRatio : svg . dataset . imgAspectRatio || null ,
7133
7220
svgAspectRatio : svgAspectRatio ,
0 commit comments