diff --git a/addons/website/static/src/js/utils.js b/addons/website/static/src/js/utils.js index b8443a1cb5a..7b137009225 100644 --- a/addons/website/static/src/js/utils.js +++ b/addons/website/static/src/js/utils.js @@ -275,6 +275,82 @@ function sendRequest(route, params) { form.submit(); } +/** + * Converts a base64 SVG into a base64 PNG. + * + * @param {string|HTMLImageElement} src - an URL to a SVG or a *loaded* image + * with such an URL. This allows the call to potentially be a bit more + * efficient in that second case. + * @returns {Promise} a base64 PNG (as result of a Promise) + */ +async function svgToPNG(src) { + function checkImg(imgEl) { + // Firefox does not support drawing SVG to canvas unless it has width + // and height attributes set on the root . + return (imgEl.naturalHeight !== 0); + } + function toPNGViaCanvas(imgEl) { + const canvas = document.createElement('canvas'); + canvas.width = imgEl.width; + canvas.height = imgEl.height; + canvas.getContext('2d').drawImage(imgEl, 0, 0); + return canvas.toDataURL('image/png'); + } + + // In case we receive a loaded image and that this image is not problematic, + // we can convert it to PNG directly. + if (src instanceof HTMLImageElement) { + const loadedImgEl = src; + if (checkImg(loadedImgEl)) { + return toPNGViaCanvas(loadedImgEl); + } + src = loadedImgEl.src; + } + + // At this point, we either did not receive a loaded image or the received + // loaded image is problematic => we have to do some asynchronous code. + return new Promise(resolve => { + const imgEl = new Image(); + imgEl.onload = () => { + if (checkImg(imgEl)) { + resolve(imgEl); + return; + } + + // Set arbitrary height on image and attach it to the DOM to force + // width computation. + imgEl.height = 1000; + imgEl.style.opacity = 0; + document.body.appendChild(imgEl); + + const request = new XMLHttpRequest(); + request.open('GET', imgEl.src, true); + request.onload = () => { + // Convert the data URI to a SVG element + const parser = new DOMParser(); + const result = parser.parseFromString(request.responseText, 'text/xml'); + const svgEl = result.getElementsByTagName("svg")[0]; + + // Add the attributes Firefox needs and remove the image from + // the DOM. + svgEl.setAttribute('width', imgEl.width); + svgEl.setAttribute('height', imgEl.height); + imgEl.remove(); + + // Convert the SVG element to a data URI + const svg64 = btoa(new XMLSerializer().serializeToString(svgEl)); + const finalImg = new Image(); + finalImg.onload = () => { + resolve(finalImg); + }; + finalImg.src = `data:image/svg+xml;base64,${svg64}`; + }; + request.send(); + }; + imgEl.src = src; + }).then(loadedImgEl => toPNGViaCanvas(loadedImgEl)); +} + return { loadAnchors: loadAnchors, autocompleteWithPages: autocompleteWithPages, @@ -282,5 +358,6 @@ return { prompt: prompt, sendRequest: sendRequest, websiteDomain: websiteDomain, + svgToPNG: svgToPNG, }; }); diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js index de60bf7fbf6..6e488328124 100644 --- a/addons/website_slides/static/src/js/slides_upload.js +++ b/addons/website_slides/static/src/js/slides_upload.js @@ -4,6 +4,7 @@ import { qweb as QWeb, _t } from 'web.core'; import Dialog from 'web.Dialog'; import publicWidget from 'web.public.widget'; import utils from 'web.utils'; +import wUtils from 'website.utils'; var SlideUploadDialog = Dialog.extend({ template: 'website.slide.upload.modal', @@ -124,7 +125,7 @@ var SlideUploadDialog = Dialog.extend({ * * @private */ - _formValidateGetValues: function (forcePublished) { + _formValidateGetValues: async function (forcePublished) { var canvas = this.$('#data_canvas')[0]; var values = _.extend({ 'channel_id': this.channelID, @@ -150,18 +151,19 @@ var SlideUploadDialog = Dialog.extend({ } else if (values['slide_type'] === 'webpage') { _.extend(values, { 'mime_type': 'text/html', - 'image_1920': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data, + 'image_1920': this.file.type === 'image/svg+xml' ? await this._svgToPNG() : this.file.data, }); } else if (/^image\/.*/.test(this.file.type)) { + const fileData = this.file.type === 'image/svg+xml' ? await this._svgToPNG() : this.file.data; if (values['slide_type'] === 'presentation') { _.extend(values, { 'slide_type': 'infographic', 'mime_type': this.file.type === 'image/svg+xml' ? 'image/png' : this.file.type, - 'datas': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data + 'datas': fileData, }); } else { _.extend(values, { - 'image_1920': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data, + 'image_1920': fileData, }); } } @@ -364,18 +366,18 @@ var SlideUploadDialog = Dialog.extend({ this.$('.o_slide_preview').addClass('d-none'); }, /** - * @private + * TODO: Remove this part, as now SVG support in image resize tools is + * included? + * Python PIL does not support SVG, so converting SVG to PNG. + * + * @returns {Promise} */ - // TODO: Remove this part, as now SVG support in image resize tools is included - //Python PIL does not support SVG, so converting SVG to PNG - _svgToPng: function () { - var img = this.$el.find('img#slide-image')[0]; - var canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - canvas.getContext('2d').drawImage(img, 0, 0); - return canvas.toDataURL('image/png').split(',')[1]; + _svgToPNG: async function () { + const imgEl = this.$el.find('img#slide-image')[0]; + const png = await wUtils.svgToPNG(imgEl); + return png.split(',')[1]; }, + //-------------------------------------------------------------------------- // Handler //-------------------------------------------------------------------------- @@ -589,21 +591,27 @@ var SlideUploadDialog = Dialog.extend({ this.modulesToInstallStatus = null; } }, - - _onClickFormSubmit: function (ev) { - var self = this; - var $btn = $(ev.currentTarget); - if (this._formValidate()) { - var values = this._formValidateGetValues($btn.hasClass('o_w_slide_upload_published')); // get info before changing state - var oldType = this.get('state'); - this.set('state', '_upload'); - return this._rpc({ - route: '/slides/add_slide', - params: values, - }).then(function (data) { - self._onFormSubmitDone(data, oldType); - }); + /** + * @private + * @returns {Promise} + */ + _onClickFormSubmit: async function (ev) { + if (!this._formValidate()) { + return; } + + // Get info before changing state + const forcePublished = $(ev.currentTarget).hasClass('o_w_slide_upload_published'); + const values = await this._formValidateGetValues(forcePublished); + + var oldType = this.get('state'); + this.set('state', '_upload'); + + const data = await this._rpc({ + route: '/slides/add_slide', + params: values, + }); + this._onFormSubmitDone(data, oldType); }, _onFormSubmitDone: function (data, oldType) {