forked from OCA/storage
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfields.py
228 lines (200 loc) · 8.93 KB
/
fields.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# 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