[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
<img src="image_data_uri(some_b64value)" />
```

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
<span t-field="object.image_field" t-options-widget="'image'" t-options-qweb_img_raw_data="1" />
```

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) <bso@odoo.com>
Signed-off-by: Lucas Perais (lpe) <lpe@odoo.com>
This commit is contained in:
Lucas Perais
2024-04-29 08:07:25 +00:00
parent 7a124de438
commit ccddb8a36f
2 changed files with 58 additions and 4 deletions
+44
View File
@@ -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": """<t t-name="test_qweb">
<span t-field="record.flag_image" t-options-widget="'image'" t-options-qweb_img_raw_data="is_raw_image" />
</t>"""
})
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())
+14 -4
View File
@@ -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'))