From ccddb8a36fdf4cc53af26963837d10ad7a906a46 Mon Sep 17 00:00:00 2001 From: Lucas Perais Date: Mon, 15 Apr 2024 16:40:43 +0200 Subject: [PATCH] [FIX] base: ir_qweb_field:image: handle webp mimetype See discussions on https://github.com/odoo/odoo/pull/85494/. TLDR: webp image format needs to be supported, but we should avoid going through the Pillow library as it is largely unsafe for that format. jpg attachment are created in JS at upload time. Wkhtmltopdf doesn't support webp, so, in reports, we should display one of those jpg copies. This work is handled by `ir.qweb: _get_converted_image_data_uri` which is used as: ```xml ``` The mentioned PR did not however adapt the ir.qweb.field.image that, when passed the option `qweb_img_raw_data` should return a base64 url such as `data:[mimetype],base64,[datas]`. usage: ```xml ``` Hence, before this commit, there was a crash as we tried to pass that value to PIL. After this commit, there is no crash, and the image displays correctly as JPG in the PDF opw-3859423 closes odoo/odoo#163003 X-original-commit: 9056a4b1f28e820c0444f28367cc30abebd5ea30 Signed-off-by: Benoit Socias (bso) Signed-off-by: Lucas Perais (lpe) --- addons/web/tests/test_ir_qweb.py | 44 +++++++++++++++++++++++ odoo/addons/base/models/ir_qweb_fields.py | 18 +++++++--- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/addons/web/tests/test_ir_qweb.py b/addons/web/tests/test_ir_qweb.py index 06d3bd3e724..aa1586cb4e5 100644 --- a/addons/web/tests/test_ir_qweb.py +++ b/addons/web/tests/test_ir_qweb.py @@ -1,5 +1,8 @@ +import base64 from lxml import etree + from odoo.tests.common import TransactionCase +from odoo.tools.mimetypes import guess_mimetype class TestIrQweb(TransactionCase): def test_image_field(self): @@ -28,3 +31,44 @@ class TestIrQweb(TransactionCase): self.assertTrue(img.get("src").startswith("/web/image")) self.assertEqual(img.get("class"), "img img-fluid") self.assertEqual(img.get("alt"), "test image partner") + + def test_image_field_webp(self): + webp = "UklGRsCpAQBXRUJQVlA4WAoAAAAQAAAAGAQA/wMAQUxQSMywAAAdNANp22T779/0RUREkvqLOTPesG1T21jatpLTSbpXQzTMEw3zWMM81jCPnWG2fTM7vpndvpkd38y2758Y+6a/Ld/Mt3zzT/XwzCKlV0Ooo61UpZIsKLjKc98R" + webp_decoded = base64.b64decode(webp) + self.assertEqual(guess_mimetype(webp_decoded), "image/webp") + + view = self.env["ir.ui.view"].create({ + "key": "web.test_qweb", + "type": "qweb", + "arch": """ + + """ + }) + lang_record = self.env["res.lang"].create({ + "name": "test lang", + "flag_image": webp, + "code": "TEST" + }) + attachment = self.env["ir.attachment"].search([ + ("res_model", "=", "res.lang"), + ("res_id", '=', lang_record.id), + ("res_field", "=", "flag_image") + ]) + + jpeg_attach = self.env["ir.attachment"].create({ + "name": "webpcopy.jpg", + "res_model": "ir.attachment", + "res_id": attachment.id, + "datas": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVR4nGJxKFrEwMDAxAAGgAAAAP//D+IBWx9K7TUAAAAASUVORK5CYII=" + }) + jpeg_datas = jpeg_attach.datas + + html = view.with_context(webp_as_jpg=False)._render_template(view.id, {"is_raw_image": True, "record": lang_record}) + tree = etree.fromstring(html) + img = tree.find("img") + self.assertEqual(img.get("src"), "data:image/webp;base64,%s" % webp) + + html = view.with_context(webp_as_jpg=True)._render_template(view.id, {"is_raw_image": True, "record": lang_record}) + tree = etree.fromstring(html) + img = tree.find("img") + self.assertEqual(img.get("src"), "data:image/png;base64,%s" % jpeg_datas.decode()) diff --git a/odoo/addons/base/models/ir_qweb_fields.py b/odoo/addons/base/models/ir_qweb_fields.py index a65e4dd6861..d0e3b9577f3 100644 --- a/odoo/addons/base/models/ir_qweb_fields.py +++ b/odoo/addons/base/models/ir_qweb_fields.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import base64 +import binascii from datetime import time import logging import re @@ -15,6 +16,7 @@ from odoo import api, fields, models, _, _lt, tools from odoo.tools import posix_to_ldml, float_utils, format_date, format_duration, pycompat from odoo.tools.mail import safe_attrs from odoo.tools.misc import get_lang, babel_locale_parse +from odoo.tools.mimetypes import guess_mimetype _logger = logging.getLogger(__name__) @@ -390,13 +392,21 @@ class ImageConverter(models.AbstractModel): @api.model def _get_src_data_b64(self, value, options): - try: # FIXME: maaaaaybe it could also take raw bytes? - image = Image.open(BytesIO(base64.b64decode(value))) + try: + img_b64 = base64.b64decode(value) + except binascii.Error: + raise ValueError("Invalid image content") from None + + if img_b64 and guess_mimetype(img_b64, '') == 'image/webp': + return self.env["ir.qweb"]._get_converted_image_data_uri(value) + + try: + image = Image.open(BytesIO(img_b64)) image.verify() except IOError: - raise ValueError("Non-image binary fields can not be converted to HTML") + raise ValueError("Non-image binary fields can not be converted to HTML") from None except: # image.verify() throws "suitable exceptions", I have no idea what they are - raise ValueError("Invalid image content") + raise ValueError("Invalid image content") from None return "data:%s;base64,%s" % (Image.MIME[image.format], value.decode('ascii'))