Initial import from Docker volume
This commit is contained in:
214
ir/avatar.py
Executable file
214
ir/avatar.py
Executable file
@@ -0,0 +1,214 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
import uuid
|
||||
from random import Random
|
||||
from urllib.parse import quote, urljoin
|
||||
|
||||
try:
|
||||
import PIL
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
except ImportError:
|
||||
PIL = None
|
||||
|
||||
from trytond.config import config
|
||||
from trytond.model import ModelSQL, Unique, fields
|
||||
from trytond.pool import Pool
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.wsgi import Base64Converter
|
||||
|
||||
from .resource import ResourceMixin
|
||||
|
||||
if config.getboolean('database', 'avatar_filestore', default=False):
|
||||
file_id = 'image_id'
|
||||
store_prefix = config.get('database', 'avatar_prefix', default=None)
|
||||
else:
|
||||
file_id = None
|
||||
store_prefix = None
|
||||
URL_BASE = config.get('web', 'avatar_base', default='')
|
||||
FONT = os.path.join(os.path.dirname(__file__), 'fonts', 'karla.ttf')
|
||||
|
||||
|
||||
class ImageMixin:
|
||||
__slots__ = ()
|
||||
image = fields.Binary(
|
||||
"Image", file_id=file_id, store_prefix=store_prefix)
|
||||
image_id = fields.Char("Image ID", readonly=True)
|
||||
|
||||
|
||||
class Avatar(ImageMixin, ResourceMixin, ModelSQL):
|
||||
"Avatar"
|
||||
__name__ = 'ir.avatar'
|
||||
|
||||
uuid = fields.Char("UUID", required=True)
|
||||
cache = fields.One2Many('ir.avatar.cache', 'avatar', "Cache")
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
t = cls.__table__()
|
||||
cls._sql_constraints = [
|
||||
('resource_unique', Unique(t, t.resource),
|
||||
'ir.msg_avatar_resource_unique'),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def default_uuid(cls):
|
||||
return uuid.uuid4().hex
|
||||
|
||||
@classmethod
|
||||
def create(cls, vlist):
|
||||
vlist = [v.copy() for v in vlist]
|
||||
for values in vlist:
|
||||
values.setdefault('uuid', cls.default_uuid())
|
||||
return super().create(vlist)
|
||||
|
||||
@classmethod
|
||||
def write(cls, *args):
|
||||
avatars = sum(args[0:None:2], [])
|
||||
super().write(*args)
|
||||
cls.clear_cache(avatars)
|
||||
|
||||
@classmethod
|
||||
def copy(cls, avatars, default=None):
|
||||
if default is None:
|
||||
default = {}
|
||||
else:
|
||||
default = default.copy()
|
||||
default.setdefault('uuid', None)
|
||||
default.setdefault('cache', None)
|
||||
return super().copy(avatars, default=default)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
if self.image_id or self.image:
|
||||
return urljoin(
|
||||
URL_BASE, quote('/avatar/%(database)s/%(uuid)s' % {
|
||||
'database': Base64Converter(None).to_url(
|
||||
Transaction().database.name),
|
||||
'uuid': self.uuid,
|
||||
}))
|
||||
|
||||
def get(self, size=64):
|
||||
size = min((
|
||||
2 ** math.ceil(math.log2(size)),
|
||||
10 * math.ceil(size / 10) if size <= 100
|
||||
else 50 * math.ceil(size / 50)))
|
||||
if not (0 < size <= 2048):
|
||||
raise ValueError("Invalid size")
|
||||
for avatar in self.cache:
|
||||
if avatar.size == size:
|
||||
return avatar.image
|
||||
if not self.image:
|
||||
return None
|
||||
if PIL:
|
||||
with Transaction().new_transaction():
|
||||
cache = self._store_cache(size, self._resize(size))
|
||||
# Save cache only if record is already committed
|
||||
if self.__class__.search([('id', '=', self.id)]):
|
||||
cache.save()
|
||||
return cache.image
|
||||
else:
|
||||
return self.image
|
||||
|
||||
@classmethod
|
||||
def convert(cls, image, **_params):
|
||||
if not PIL or not image:
|
||||
return image
|
||||
data = io.BytesIO()
|
||||
img = Image.open(io.BytesIO(image))
|
||||
width, height = img.size
|
||||
size = min(width, height)
|
||||
img = img.crop((
|
||||
(width - size) // 2,
|
||||
(height - size) // 2,
|
||||
(width + size) // 2,
|
||||
(height + size) // 2))
|
||||
if size > 2048:
|
||||
img = img.resize((2048, 2048))
|
||||
if img.mode in {'RGBA', 'P'}:
|
||||
img.convert('RGBA')
|
||||
background = Image.new('RGBA', img.size, (255, 255, 255))
|
||||
background.alpha_composite(img)
|
||||
img = background.convert('RGB')
|
||||
img.save(data, format='jpeg', optimize=True, **_params)
|
||||
return data.getvalue()
|
||||
|
||||
def _resize(self, size=64, **_params):
|
||||
if not PIL:
|
||||
return self.image
|
||||
data = io.BytesIO()
|
||||
img = Image.open(io.BytesIO(self.image))
|
||||
img = img.resize((size, size))
|
||||
img.save(data, format='jpeg', optimize=True, **_params)
|
||||
return data.getvalue()
|
||||
|
||||
def _store_cache(self, size, image):
|
||||
pool = Pool()
|
||||
Cache = pool.get('ir.avatar.cache')
|
||||
return Cache(
|
||||
avatar=self,
|
||||
image=image,
|
||||
size=size)
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls, avatars):
|
||||
pool = Pool()
|
||||
Cache = pool.get('ir.avatar.cache')
|
||||
caches = [c for a in avatars for c in a.cache]
|
||||
Cache.delete(caches)
|
||||
|
||||
|
||||
class AvatarCache(ImageMixin, ModelSQL):
|
||||
"Avatar Cache"
|
||||
__name__ = 'ir.avatar.cache'
|
||||
|
||||
avatar = fields.Many2One(
|
||||
'ir.avatar', "Avatar", required=True, ondelete='CASCADE')
|
||||
size = fields.Integer(
|
||||
"Size", required=True,
|
||||
domain=[
|
||||
('size', '>', 0),
|
||||
('size', '<=', 2048),
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
t = cls.__table__()
|
||||
cls._sql_constraints = [
|
||||
('size_unique', Unique(t, t.avatar, t.size),
|
||||
'ir.msg_avatar_size_unique'),
|
||||
]
|
||||
cls._order.append(('size', 'ASC'))
|
||||
|
||||
|
||||
def generate(size, string):
|
||||
if not PIL:
|
||||
return
|
||||
|
||||
def background_color(string):
|
||||
random = Random(string)
|
||||
r = v = b = 255
|
||||
# Skip too bright color
|
||||
while r + v + b > 255 * 2:
|
||||
r = random.randint(0, 255)
|
||||
v = random.randint(0, 255)
|
||||
b = random.randint(0, 255)
|
||||
return r, v, b
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype(FONT, size=int(0.65 * size))
|
||||
except ImportError:
|
||||
return
|
||||
white = (255, 255, 255)
|
||||
image = Image.new('RGB', (size, size), background_color(string))
|
||||
draw = ImageDraw.Draw(image)
|
||||
letter = string[0].upper() if string else ''
|
||||
draw.text(
|
||||
(size / 2, size / 2), letter, fill=white, font=font, anchor='mm')
|
||||
data = io.BytesIO()
|
||||
image.save(data, format='jpeg', optimize=True)
|
||||
return data.getvalue()
|
||||
Reference in New Issue
Block a user