From eb1262960f54fd81424a86ef4d6693a950e76cb4 Mon Sep 17 00:00:00 2001 From: xO-Tx Date: Thu, 29 Jul 2021 13:02:30 +0000 Subject: [PATCH] [IMP] web_editor: allow adding a shape-on-image via custom URL (themes) The goal of this commit is to add a "Python version" of the shape-on-image feature using a controller. Since the whole logic to apply shapes on image is JS-based, we need to use this URL (just like for background shapes) when we want to add a shape by default on images in themes. When the configurator replaces a snippet image (which the theme defines to have a shape), the shape option should still be applied on the new image. On the JS side, loadImageInfo() is overridden in order to mark images (with theme default shapes) with corresponding attachment data (original-id, original-src, mimetype). Here is an example of the minimum xpath required to add a shape on a snippet image in a theme: ``` ``` task-2593454 closes odoo/odoo#73938 Signed-off-by: Quentin Smetz (qsm) --- addons/web_editor/controllers/main.py | 117 +++++++++++++----- .../static/src/js/editor/image_processing.js | 6 +- .../static/src/js/editor/snippets.options.js | 40 ++++-- 3 files changed, 120 insertions(+), 43 deletions(-) diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 1726a5f128c..e5b27b4c92f 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -19,6 +19,9 @@ from odoo.exceptions import UserError from odoo.modules.module import get_module_path, get_resource_path from odoo.tools.misc import file_open from odoo.tools.mimetypes import guess_mimetype +from odoo.tools.image import image_data_uri, base64_to_image +from odoo.addons.web.controllers.main import Binary +from odoo.addons.base.models.assetsbundle import AssetsBundle from ..models.ir_attachment import SUPPORTED_IMAGE_EXTENSIONS, SUPPORTED_IMAGE_MIMETYPES @@ -521,6 +524,57 @@ class Web_Editor(http.Controller): attachment.generate_access_token() return '%s?access_token=%s' % (attachment.image_src, attachment.access_token) + def _get_shape_svg(self, module, *segments): + shape_path = get_resource_path(module, 'static', *segments) + if not shape_path: + raise werkzeug.exceptions.NotFound() + with tools.file_open(shape_path, 'r', filter_ext=('.svg',)) as file: + return file.read() + + def _update_svg_colors(self, options, svg): + user_colors = [] + svg_options = {} + default_palette = { + '1': '#3AADAA', + '2': '#7C6576', + '3': '#F6F6F6', + '4': '#FFFFFF', + '5': '#383E45', + } + bundle_css = None + regex_hex = r'#[0-9A-F]{6,8}' + regex_rgba = r'rgba?\(\d{1,3},\d{1,3},\d{1,3}(?:,[0-9.]{1,4})?\)' + for key, value in options.items(): + colorMatch = re.match('^c([1-5])$', key) + if colorMatch: + css_color_value = value + # Check that color is hex or rgb(a) to prevent arbitrary injection + if not re.match(r'(?i)^%s$|^%s$' % (regex_hex, regex_rgba), css_color_value.replace(' ', '')): + if re.match('^o-color-([1-5])$', css_color_value): + if not bundle_css: + bundle = 'web.assets_frontend' + files, _ = request.env["ir.qweb"]._get_asset_content(bundle, options=request.context) + asset = AssetsBundle(bundle, files) + bundle_css = asset.css().index_content + color_search = re.search(r'(?i)--%s:\s+(%s|%s)' % (css_color_value, regex_hex, regex_rgba), bundle_css) + if not color_search: + raise werkzeug.exceptions.BadRequest() + css_color_value = color_search.group(1) + else: + raise werkzeug.exceptions.BadRequest() + user_colors.append([tools.html_escape(css_color_value), colorMatch.group(1)]) + else: + svg_options[key] = value + + color_mapping = {default_palette[palette_number]: color for color, palette_number in user_colors} + # create a case-insensitive regex to match all the colors to replace, eg: '(?i)(#3AADAA)|(#7C6576)' + regex = '(?i)%s' % '|'.join('(%s)' % color for color in color_mapping.keys()) + + def subber(match): + key = match.group().upper() + return color_mapping[key] if key in color_mapping else key + return re.sub(regex, subber, svg), svg_options + @http.route(['/web_editor/shape//'], type='http', auth="public", website=True) def shape(self, module, filename, **kwargs): """ @@ -536,43 +590,38 @@ class Web_Editor(http.Controller): raise werkzeug.exceptions.NotFound() svg = b64decode(attachment.datas).decode('utf-8') else: - shape_path = get_resource_path(module, 'static', 'shapes', filename) - if not shape_path: - raise werkzeug.exceptions.NotFound() - with tools.file_open(shape_path, 'r') as file: - svg = file.read() + svg = self._get_shape_svg(module, 'shapes', filename) - user_colors = [] - for key, value in kwargs.items(): - colorMatch = re.match('^c([1-5])$', key) - if colorMatch: - # Check that color is hex or rgb(a) to prevent arbitrary injection - if not re.match(r'(?i)^#[0-9A-F]{6,8}$|^rgba?\(\d{1,3},\d{1,3},\d{1,3}(?:,[0-9.]{1,4})?\)$', value.replace(' ', '')): - raise werkzeug.exceptions.BadRequest() - user_colors.append([tools.html_escape(value), colorMatch.group(1)]) - elif key == 'flip': - if value == 'x': - svg = svg.replace('//'], type='http', auth="public", website=True) + def image_shape(self, module, filename, img_key, **kwargs): + svg = self._get_shape_svg(module, 'image_shapes', filename) + _, _, image_base64 = request.env['ir.http'].binary_content( + xmlid=img_key, model='ir.attachment', field='datas', default_mimetype='image/png') + if not image_base64: + image_base64 = b64encode(Binary.placeholder()) + image = base64_to_image(image_base64) + width, height = tuple(str(size) for size in image.size) + root = etree.fromstring(svg) + root.attrib.update({'width': width, 'height': height}) + # Update default color palette on shape SVG. + svg, _ = self._update_svg_colors(kwargs, etree.tostring(root, pretty_print=True).decode('utf-8')) + # Add image in base64 inside the shape. + uri = image_data_uri(image_base64) + svg = svg.replace(' color !== null ? this._getCSSColorValue(`o-color-${(i + 1)}`) : null); } - newColors.forEach((color, i) => shape = shape.replace(new RegExp(oldColors[i], 'g'), color)); + newColors.forEach((color, i) => shape = shape.replace(new RegExp(oldColors[i], 'g'), this._getCSSColorValue(color))); await this._writeShape(shape); if (save) { img.dataset.shapeColors = newColors.join(';'); @@ -4792,11 +4800,29 @@ registry.ImageOptimize = ImageHandlerOption.extend({ * @returns {string} */ _getCSSColorValue(color) { - if (ColorpickerWidget.isCSSColor(color)) { + if (!color || ColorpickerWidget.isCSSColor(color)) { return color; } return weUtils.getCSSVariableValue(color); }, + /** + * Overridden to set attachment data on theme images (with default shapes). + * + * @override + * @private + */ + async _initializeImage() { + const img = this._getImg(); + const match = img.src.match(/\/web_editor\/image_shape\/(\w+\.\w+)/); + if (img.dataset.shape && match) { + await this._loadImageInfo(`/web/image/${match[1]}`); + // Image data-mimetype should be changed to SVG since loadImageInfo() + // will set the original attachment mimetype on it. + img.dataset.mimetype = 'image/svg+xml'; + return; + } + return this._super(...arguments); + }, //-------------------------------------------------------------------------- // Handlers