Setting width *and* height attributes allows to reserve some space to avoid layout shift during page loading. Of course, CSS rules set the height the user chose, while the width is set to 'auto'. But while the image is loading, it is best to already reserve some width to reduce layout shift (like making the menu move or even re-render itself into a "+" menu). The chosen values for the space reservation are the ones of the default logo and theme, but it does not really matter as long as they are coherent. While the image is being loaded, the chosen user height is still applied and the 'auto' width rule induces a width that respects the aspect ratio set by the width and height attributes. That could be a problem if the real logo has a larger height than width, in which case the layout shift would be increased because of the arbitrary values set as width and height, but in most cases, this should reduce it. This also allows to gain some page speed scoring. Examples: =# Logo 200 x 100, height set to 50 Before this commit - while loading: width = 0, height = 50 - once loaded: width = 200 / 100 * 50 = 100, height = 50 -> layout shift of 100 - 0 = 100 After this commit - while loading: width = 95 / 40 * 50 = 118.75, height = 50 - once loaded: width = 200 / 100 * 50 = 100, height = 50 -> layout shift of 100 - 118.75 = -18.75 => Shift of 18.75px to the left, way better than shift of 100px to the right =# Logo 100 x 100, height set to 50 Before this commit - while loading: width = 0, height = 50 - once loaded: width = 100 / 100 * 50 = 50, height = 50 -> layout shift of 50 - 0 = 50 After this commit - while loading: width = 95 / 40 * 50 = 118.75, height = 50 - once loaded: width = 100 / 100 * 50 = 50, height = 50 -> layout shift of 50 - 118.75 = -68.75 => Shift of 68.75px to the left, kinda the same as a shift of 50px to the right. =# Logo 100 x 200, height set to 50 Before this commit - while loading: width = 0, height = 50 - once loaded: width = 100 / 200 * 50 = 25, height = 50 -> layout shift of 25 - 0 = 25 After this commit - while loading: width = 95 / 40 * 50 = 118.75, height = 50 - once loaded: width = 100 / 200 * 50 = 25, height = 50 -> layout shift of 25 - 118.75 = -93.75 => Shift of 93.75px to the left, worse than shift of 25px to the right but the case of having a 'portrait' logo is considered less common than having a 'landscape' logo. Ideally, we should choose the arbitrary values related to the most common aspect ratio. closes odoo/odoo#101149 X-original-commit: 73786c5c45202914e02325c19a2afaa47d31c341 Signed-off-by: Romain Derie (rde) <rde@odoo.com>
120 lines
4.3 KiB
Python
120 lines
4.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import hashlib
|
|
from collections import OrderedDict
|
|
from werkzeug.urls import url_quote
|
|
from markupsafe import Markup as M
|
|
|
|
from odoo import api, models
|
|
from odoo.tools import pycompat
|
|
from odoo.tools import html_escape as escape
|
|
|
|
|
|
class Image(models.AbstractModel):
|
|
"""
|
|
Widget options:
|
|
|
|
``class``
|
|
set as attribute on the generated <img> tag
|
|
"""
|
|
_name = 'ir.qweb.field.image'
|
|
_description = 'Qweb Field Image'
|
|
_inherit = 'ir.qweb.field.image'
|
|
|
|
def _get_src_urls(self, record, field_name, options):
|
|
"""Considering the rendering options, returns the src and data-zoom-image urls.
|
|
|
|
:return: src, src_zoom urls
|
|
:rtype: tuple
|
|
"""
|
|
max_size = None
|
|
if options.get('resize'):
|
|
max_size = options.get('resize')
|
|
else:
|
|
max_width, max_height = options.get('max_width', 0), options.get('max_height', 0)
|
|
if max_width or max_height:
|
|
max_size = '%sx%s' % (max_width, max_height)
|
|
|
|
sha = hashlib.sha512(str(getattr(record, '__last_update')).encode('utf-8')).hexdigest()[:7]
|
|
max_size = '' if max_size is None else '/%s' % max_size
|
|
|
|
if options.get('filename-field') and getattr(record, options['filename-field'], None):
|
|
filename = record[options['filename-field']]
|
|
elif options.get('filename'):
|
|
filename = options['filename']
|
|
else:
|
|
filename = record.display_name
|
|
filename = (filename or 'name').replace('/', '-').replace('\\', '-').replace('..', '--')
|
|
|
|
src = '/web/image/%s/%s/%s%s/%s?unique=%s' % (record._name, record.id, options.get('preview_image', field_name), max_size, url_quote(filename), sha)
|
|
|
|
src_zoom = None
|
|
if options.get('zoom') and getattr(record, options['zoom'], None):
|
|
src_zoom = '/web/image/%s/%s/%s%s/%s?unique=%s' % (record._name, record.id, options['zoom'], max_size, url_quote(filename), sha)
|
|
elif options.get('zoom'):
|
|
src_zoom = options['zoom']
|
|
|
|
return src, src_zoom
|
|
|
|
@api.model
|
|
def record_to_html(self, record, field_name, options):
|
|
assert options['tagName'] != 'img',\
|
|
"Oddly enough, the root tag of an image field can not be img. " \
|
|
"That is because the image goes into the tag, or it gets the " \
|
|
"hose again."
|
|
|
|
if options.get('qweb_img_raw_data', False):
|
|
return super(Image, self).record_to_html(record, field_name, options)
|
|
|
|
aclasses = ['img', 'img-fluid'] if options.get('qweb_img_responsive', True) else ['img']
|
|
aclasses += options.get('class', '').split()
|
|
classes = ' '.join(map(escape, aclasses))
|
|
|
|
src, src_zoom = self._get_src_urls(record, field_name, options)
|
|
|
|
if options.get('alt-field') and getattr(record, options['alt-field'], None):
|
|
alt = escape(record[options['alt-field']])
|
|
elif options.get('alt'):
|
|
alt = options['alt']
|
|
else:
|
|
alt = escape(record.display_name)
|
|
|
|
itemprop = None
|
|
if options.get('itemprop'):
|
|
itemprop = options['itemprop']
|
|
|
|
atts = OrderedDict()
|
|
atts["src"] = src
|
|
atts["itemprop"] = itemprop
|
|
atts["class"] = classes
|
|
atts["style"] = options.get('style')
|
|
atts["width"] = options.get('width')
|
|
atts["height"] = options.get('height')
|
|
atts["alt"] = alt
|
|
atts["data-zoom"] = src_zoom and u'1' or None
|
|
atts["data-zoom-image"] = src_zoom
|
|
atts["data-no-post-process"] = options.get('data-no-post-process')
|
|
|
|
atts = self.env['ir.qweb']._post_processing_att('img', atts)
|
|
|
|
img = ['<img']
|
|
for name, value in atts.items():
|
|
if value:
|
|
img.append(' ')
|
|
img.append(escape(pycompat.to_text(name)))
|
|
img.append('="')
|
|
img.append(escape(pycompat.to_text(value)))
|
|
img.append('"')
|
|
img.append('/>')
|
|
|
|
return M(''.join(img))
|
|
|
|
class ImageUrlConverter(models.AbstractModel):
|
|
_description = 'Qweb Field Image'
|
|
_inherit = 'ir.qweb.field.image_url'
|
|
|
|
def _get_src_urls(self, record, field_name, options):
|
|
image_url = record[options.get('preview_image', field_name)]
|
|
return image_url, options.get("zoom", None)
|