mirror of
https://github.com/bringout/oca-storage.git
synced 2026-04-19 08:52:01 +02:00
228 lines
8.9 KiB
Python
228 lines
8.9 KiB
Python
# Copyright 2023 ACSONE SA/NV
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
# pylint: disable=method-required-super
|
|
from contextlib import contextmanager
|
|
from io import BytesIO, IOBase
|
|
|
|
from odoo import _
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools.image import image_process
|
|
|
|
from odoo.addons.fs_attachment.models.ir_attachment import IrAttachment
|
|
from odoo.addons.fs_file.fields import FSFile, FSFileValue
|
|
|
|
|
|
class FSImageValue(FSFileValue):
|
|
"""Value for the FSImage field"""
|
|
|
|
def __init__(
|
|
self,
|
|
attachment: IrAttachment = None,
|
|
name: str = None,
|
|
value: bytes | IOBase = None,
|
|
alt_text: str = None,
|
|
) -> None:
|
|
super().__init__(attachment, name, value)
|
|
if self._attachment and alt_text is not None:
|
|
raise ValueError(
|
|
"FSImageValue cannot be initialized with an attachment and an"
|
|
" alt_text at the same time. When initializing with an attachment,"
|
|
" you can't pass any other argument."
|
|
)
|
|
self._alt_text = alt_text
|
|
|
|
@property
|
|
def alt_text(self) -> str:
|
|
alt_text = self._attachment.alt_text if self._attachment else self._alt_text
|
|
return alt_text
|
|
|
|
@alt_text.setter
|
|
def alt_text(self, value: str) -> None:
|
|
if self._attachment:
|
|
self._attachment.alt_text = value
|
|
else:
|
|
self._alt_text = value
|
|
|
|
@classmethod
|
|
def from_fs_file_value(cls, fs_file_value: FSFileValue) -> "FSImageValue":
|
|
if isinstance(fs_file_value, FSImageValue):
|
|
return fs_file_value
|
|
return cls(
|
|
attachment=fs_file_value.attachment,
|
|
name=fs_file_value.name if not fs_file_value.attachment else None,
|
|
value=fs_file_value._buffer
|
|
if not fs_file_value.attachment
|
|
else fs_file_value._buffer,
|
|
)
|
|
|
|
def image_process(
|
|
self,
|
|
size=(0, 0),
|
|
verify_resolution=False,
|
|
quality=0,
|
|
crop=None,
|
|
colorize=False,
|
|
output_format="",
|
|
):
|
|
"""
|
|
Process the image to adapt it to the given parameters.
|
|
:param size: a tuple (max_width, max_height) containing the maximum
|
|
width and height of the processed image.
|
|
If one of the value is 0, it will be calculated to keep the aspect
|
|
ratio.
|
|
If both values are 0, the image will not be resized.
|
|
:param verify_resolution: if True, make sure the original image size is not
|
|
excessive before starting to process it. The max allowed resolution is
|
|
defined by `IMAGE_MAX_RESOLUTION` in :class:`odoo.tools.image.ImageProcess`.
|
|
:param int quality: quality setting to apply. Default to 0.
|
|
|
|
- for JPEG: 1 is worse, 95 is best. Values above 95 should be
|
|
avoided. Falsy values will fallback to 95, but only if the image
|
|
was changed, otherwise the original image is returned.
|
|
- for PNG: set falsy to prevent conversion to a WEB palette.
|
|
- for other formats: no effect.
|
|
:param crop: (True | 'top' | 'bottom'):
|
|
* True, the image will be cropped to the given size.
|
|
* 'top', the image will be cropped at the top to the given size.
|
|
* 'bottom', the image will be cropped at the bottom to the given size.
|
|
Otherwise, it will be resized to fit the given size.
|
|
:param colorize: if True, the transparent background of the image
|
|
will be colorized in a random color.
|
|
:param str output_format: the output format. Can be PNG, JPEG, GIF, or ICO.
|
|
Default to the format of the original image. BMP is converted to
|
|
PNG, other formats than those mentioned above are converted to JPEG.
|
|
:return: the processed image as bytes
|
|
"""
|
|
return image_process(
|
|
self.getvalue(),
|
|
size=size,
|
|
crop=crop,
|
|
quality=quality,
|
|
verify_resolution=verify_resolution,
|
|
colorize=colorize,
|
|
output_format=output_format,
|
|
)
|
|
|
|
|
|
class FSImage(FSFile):
|
|
"""
|
|
This field is a FSFile field with an alt_text attribute used to encapsulate
|
|
an image file stored in a filesystem storage.
|
|
|
|
It's inspired by the 'image' field of odoo :class:`odoo.fields.Binary` but
|
|
is designed to store the image in a filesystem storage instead of the
|
|
database.
|
|
|
|
If image size is greater than the ``max_width``/``max_height`` limit of pixels,
|
|
the image will be resized to the limit by keeping aspect ratio.
|
|
|
|
:param int max_width: the maximum width of the image (default: ``0``, no limit)
|
|
:param int max_height: the maximum height of the image (default: ``0``, no limit)
|
|
:param bool verify_resolution: whether the image resolution should be verified
|
|
to ensure it doesn't go over the maximum image resolution
|
|
(default: ``True``).
|
|
See :class:`odoo.tools.image.ImageProcess` for maximum image resolution
|
|
(default: ``50e6``).
|
|
"""
|
|
|
|
type = "fs_image"
|
|
|
|
max_width = 0
|
|
max_height = 0
|
|
verify_resolution = True
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._image_process_mode = False
|
|
|
|
def create(self, record_values):
|
|
with self._set_image_process_mode():
|
|
return super().create(record_values)
|
|
|
|
def write(self, records, value):
|
|
if isinstance(value, dict) and "content" not in value:
|
|
# we are writing on the alt_text field only
|
|
return self._update_alt_text(records, value)
|
|
with self._set_image_process_mode():
|
|
return super().write(records, value)
|
|
|
|
def convert_to_cache(self, value, record, validate=True):
|
|
if not value:
|
|
return None
|
|
if isinstance(value, FSImageValue):
|
|
cache_value = value
|
|
else:
|
|
cache_value = super().convert_to_cache(value, record, validate)
|
|
if not isinstance(cache_value, FSImageValue):
|
|
cache_value = FSImageValue.from_fs_file_value(cache_value)
|
|
if isinstance(value, dict) and "alt_text" in value:
|
|
cache_value.alt_text = value["alt_text"]
|
|
if self._image_process_mode and cache_value.is_new:
|
|
name = cache_value.name
|
|
new_value = BytesIO(self._image_process(cache_value))
|
|
cache_value._buffer = new_value
|
|
cache_value.name = name
|
|
return cache_value
|
|
|
|
def _create_attachment(self, record, cache_value):
|
|
attachment = super()._create_attachment(record, cache_value)
|
|
# odoo filter out additional fields in create method on ir.attachment
|
|
# so we need to write the alt_text after the creation
|
|
if cache_value.alt_text:
|
|
attachment.alt_text = cache_value.alt_text
|
|
return attachment
|
|
|
|
def _convert_attachment_to_cache(self, attachment: IrAttachment) -> FSImageValue:
|
|
cache_value = super()._convert_attachment_to_cache(attachment)
|
|
return FSImageValue.from_fs_file_value(cache_value)
|
|
|
|
def _image_process(self, cache_value: FSImageValue) -> bytes | None:
|
|
if self.readonly and not self.max_width and not self.max_height:
|
|
# no need to process images for computed fields, or related fields
|
|
return cache_value.getvalue()
|
|
return (
|
|
cache_value.image_process(
|
|
size=(self.max_width, self.max_height),
|
|
verify_resolution=self.verify_resolution,
|
|
)
|
|
or None
|
|
)
|
|
|
|
def convert_to_read(self, value, record, use_name_get=True) -> dict | None:
|
|
vals = super().convert_to_read(value, record, use_name_get)
|
|
if isinstance(value, FSImageValue):
|
|
vals["alt_text"] = value.alt_text or None
|
|
return vals
|
|
|
|
@contextmanager
|
|
def _set_image_process_mode(self):
|
|
self._image_process_mode = True
|
|
try:
|
|
yield
|
|
finally:
|
|
self._image_process_mode = False
|
|
|
|
def _process_related(self, value: FSImageValue):
|
|
"""Override to resize the related value before saving it on self."""
|
|
if not value:
|
|
return None
|
|
if self.readonly and not self.max_width and not self.max_height:
|
|
# no need to process images for computed fields, or related fields
|
|
# without max_width/max_height
|
|
return value
|
|
value = super()._process_related(value)
|
|
new_value = BytesIO(self._image_process(value))
|
|
return FSImageValue(value=new_value, alt_text=value.alt_text, name=value.name)
|
|
|
|
def _update_alt_text(self, records, value: dict):
|
|
for record in records:
|
|
if not record[self.name]:
|
|
raise UserError(
|
|
_(
|
|
"Cannot set alt_text on empty image (record %(record)s.%(field_name)s)",
|
|
record=record,
|
|
field_name=self.name,
|
|
)
|
|
)
|
|
record[self.name].alt_text = value["alt_text"]
|
|
return True
|