[IMP] base,mail,web: attachments adaptation for documents

This commit is the counter-part of an enteprise commit introducing
the Documents app.

Here is a summary of what has been done:
  - tweak unlink of ir_attachment to prevent unlink recursivity
    (when attachments are attached to ir_attachments)
  - improve ir_attachment kanban view
  - add several arguments to binary_content controller:
    - 'force_ext': to force the extension in the filename, base on
      mimetype
    - 'share_token' and 'share_id': to autorize download from a
      share link
  - add 'thumbnail' field on ir_attachment to optimize Kanban view
  - add 'upper_limit' argument to image to allow to bypass the
    500*500 size limit
  - DocumentViewer now handles text files

More information available on task 1853490

Co-authored-by: Pierre Paridans <app@odoo.com>
Co-authored-by: sri-odoo <sri@odoo.com>
Co-authored-by: ThanhDodeurOdoo <tso@odoo.com>
This commit is contained in:
ThanhDodeurOdoo
2018-08-13 09:11:53 +02:00
committed by Aaron Bohy
co-authored by Pierre Paridans sri-odoo
parent ad2e2cc8b5
commit 60aa9ffd01
13 changed files with 346 additions and 42 deletions
+2
View File
@@ -964,6 +964,8 @@ class Message(models.Model):
def unlink(self):
# cascade-delete attachments that are directly attached to the message (should only happen
# for mail.messages that act as parent for a standalone mail.mail record).
if not self:
return True
self.check_access_rule('unlink')
self.mapped('attachment_ids').filtered(
lambda attach: attach.res_model == self._name and (attach.res_id in self.ids or attach.res_id == 0)
+2
View File
@@ -320,6 +320,8 @@ class MailThread(models.AbstractModel):
def unlink(self):
""" Override unlink to delete messages and followers. This cannot be
cascaded, because link is done through (res_model, res_id). """
if not self:
return True
self.env['mail.message'].search([('model', '=', self._name), ('res_id', 'in', self.ids)]).unlink()
res = super(MailThread, self).unlink()
self.env['mail.followers'].sudo().search(
+2 -2
View File
@@ -43,7 +43,7 @@ var DocumentViewer = Widget.extend({
init: function (parent, attachments, activeAttachmentID) {
this._super.apply(this, arguments);
this.attachment = _.filter(attachments, function (attachment) {
var match = attachment.mimetype.match("(image|video|application/pdf)");
var match = attachment.mimetype.match("(image|video|application/pdf|text)");
if (match) {
attachment.type = match[1];
@@ -116,7 +116,7 @@ var DocumentViewer = Widget.extend({
* @param {float} angle
*/
_getTransform: function(scale, angle) {
return 'scale3d(' + scale + ', ' + scale + ', 1) rotate(' + angle + 'deg)'
return 'scale3d(' + scale + ', ' + scale + ', 1) rotate(' + angle + 'deg)';
},
/**
* Rotate image clockwise by provided angle
@@ -0,0 +1,65 @@
$o-kanban-attachement-image-size: 80px;
.o_kanban_view {
align-items: flex-start;
.o_kanban_record.o_kanban_attachment {
padding: 0;
.o_kanban_image {
width: $o-kanban-attachement-image-size;
+ div {
padding-left: $o-kanban-attachement-image-size + $o-kanban-inside-hgutter;
@include media-breakpoint-down(sm) {
padding-left: $o-kanban-attachement-image-size + $o-kanban-inside-hgutter-mobile;
}
}
.o_kanban_image_wrapper {
min-height: $o-kanban-attachement-image-size;
display: flex;
align-items: center;
justify-content: center;
}
.o_attachment_image {
@include size($o-kanban-attachement-image-size);
}
.o_image {
@include size($o-kanban-attachement-image-size*0.7);
}
}
.o_kanban_details {
.o_kanban_details_wrapper {
display: flex;
flex-direction: column;
min-height: $o-kanban-attachement-image-size;
padding: $o-kanban-inside-vgutter $o-kanban-inside-hgutter;
.o_kanban_record_title {
margin-bottom: $o-kanban-inside-vgutter*0.5;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 95%;
}
.o_kanban_record_body {
flex: 1 1 auto;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: smaller;
}
.oe_kanban_avatar {
border-radius: 4px;
border: 1px solid $component-active-color;
}
}
}
}
}
+45 -14
View File
@@ -127,7 +127,7 @@
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_message_search"/>
</record>
<!-- Add menu entry in Settings/Email -->
<menuitem name="Messages" id="menu_mail_message" parent="base.menu_email" action="action_view_mail_message"/>
@@ -140,23 +140,54 @@
<field name="mimetype"/>
<field name="type"/>
<field name="datas_fname"/>
<field name="create_uid"/>
<field name="url"/>
<field name="type"/>
<field name="create_date"/>
<field name="active"/>
<field name="name"/>
<field name="res_model_name"/>
<field name="res_name"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="oe_kanban_vignette">
<div class="oe_attachment">
<div class="oe_name">
<field name="name"/>
<div class="oe_kanban_global_area o_kanban_attachment">
<div class="o_kanban_image">
<div class="o_kanban_image_wrapper">
<t t-set="webimage" t-value="new RegExp('image.*(gif|jpeg|jpg|png)').test(record.mimetype.value)"/>
<div t-if="record.type.raw_value == 'url'" class="o_url_image fa fa-link fa-3x text-muted"/>
<img t-elif="webimage" t-attf-src="/web/image/#{record.id.raw_value}?field=thumbnail" width="100" height="100" alt="Document" class="o_attachment_image"/>
<div t-else="!webimage" class="o_image o_image_thumbnail" t-att-data-mimetype="record.mimetype.value"/>
</div>
</div>
<div class="o_kanban_details">
<div class="o_kanban_details_wrapper">
<div t-att-title="record.datas_fname.raw_value" class="o_kanban_record_title">
<t t-if="record.type.raw_value == 'url'">
<field name="name" class="o_text_overflow"/>
</t>
<t t-else="">
<field name="datas_fname" class="o_text_overflow"/>
</t>
</div>
<div t-if="record.type.raw_value == 'binary'">
<a t-attf-href="/web/content/#{record.id.raw_value}?download=1">
<t t-set="webimage" t-value="new RegExp('image.*(gif|jpeg|jpg|png)').test(record.mimetype.value)"/>
<div t-if="!webimage" class="o_image" t-att-data-mimetype="record.mimetype.value"/>
<div t-if="webimage" class="o_attachment_image">
<img t-attf-src="/web/image/#{record.id.raw_value}?download=1" alt="Download"/>
</div>
</a>
<div class="o_kanban_record_body">
<t t-if="record.res_model_name.raw_value">
<strong><field name="res_model_name"/></strong>
<t t-if="record.res_name.raw_value">
<span t-att-title="record.res_name.raw_value">: <field name="res_name"/></span>
</t>
</t>
<t t-elif="record.type.raw_value == 'url'">
<span class="o_document_url"><i class="fa fa-globe"/> <field name="url" widget="url"/></span>
</t>
<samp t-else="" class="text-muted"> </samp>
</div>
<div class="o_kanban_record_bottom">
<time class="oe_kanban_bottom_left">
<field name="create_date" widget="date"/>
</time>
<div class="oe_kanban_bottom_right">
<img t-att-src="kanban_image('res.users', 'image_small', record.create_uid.raw_value)" t-att-data-member_id="record.create_uid.raw_value" t-att-alt="record.create_uid.raw_value" width="32" height="32" class="oe_kanban_avatar"/>
</div>
</div>
</div>
</div>
+1
View File
@@ -76,6 +76,7 @@
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/systray.scss"/>
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/mail_activity.scss"/>
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/activity_view.scss"/>
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/kanban_view.scss"/>
</xpath>
</template>
+19 -12
View File
@@ -424,11 +424,13 @@ def xml2json_from_elementtree(el, preserve_whitespaces=False):
def binary_content(xmlid=None, model='ir.attachment', id=None, field='datas', unique=False,
filename=None, filename_field='datas_fname', download=False, mimetype=None,
default_mimetype='application/octet-stream', access_token=None, env=None):
default_mimetype='application/octet-stream', share_id=None, share_token=None, access_token=None,
force_ext=False, env=None):
return request.registry['ir.http'].binary_content(
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
filename_field=filename_field, download=download, mimetype=mimetype,
default_mimetype=default_mimetype, access_token=access_token, env=env)
default_mimetype=default_mimetype, share_id=share_id, share_token=share_token, access_token=access_token,
force_ext=force_ext, env=env)
#----------------------------------------------------------
# Odoo Web web Controllers
@@ -1004,11 +1006,12 @@ class Binary(http.Controller):
'/web/content/<string:model>/<int:id>/<string:field>/<string:filename>'], type='http', auth="public")
def content_common(self, xmlid=None, model='ir.attachment', id=None, field='datas',
filename=None, filename_field='datas_fname', unique=None, mimetype=None,
download=None, data=None, token=None, access_token=None, **kw):
download=None, data=None, token=None, share_id=None, share_token=None, access_token=None,
force_ext=False, **kw):
status, headers, content = binary_content(
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
filename_field=filename_field, download=download, mimetype=mimetype,
access_token=access_token)
access_token=access_token, share_id=share_id, share_token=share_token, force_ext=force_ext)
if status == 304:
response = werkzeug.wrappers.Response(status=status, headers=headers)
elif status == 301:
@@ -1042,11 +1045,12 @@ class Binary(http.Controller):
'/web/image/<int:id>-<string:unique>/<int:width>x<int:height>/<string:filename>'], type='http', auth="public")
def content_image(self, xmlid=None, model='ir.attachment', id=None, field='datas',
filename_field='datas_fname', unique=None, filename=None, mimetype=None,
download=None, width=0, height=0, crop=False, access_token=None, avoid_if_small=False):
download=None, width=0, height=0, crop=False, share_id=None, share_token=None, access_token=None, avoid_if_small=False,
upper_limit=False):
status, headers, content = binary_content(
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
filename_field=filename_field, download=download, mimetype=mimetype,
default_mimetype='image/png', access_token=access_token)
default_mimetype='image/png', share_id=share_id, share_token=share_token, access_token=access_token)
if status == 304:
return werkzeug.wrappers.Response(status=304, headers=headers)
elif status == 301:
@@ -1065,12 +1069,15 @@ class Binary(http.Controller):
content = crop_image(content, type='center', size=(width, height), ratio=(1, 1))
elif content and (width or height):
# resize maximum 500*500
if width > 500:
width = 500
if height > 500:
height = 500
content = odoo.tools.image_resize_image(base64_source=content, size=(width or None, height or None), encoding='base64', filetype='PNG', avoid_if_small=avoid_if_small)
if not upper_limit:
# resize maximum 500*500
if width > 500:
width = 500
if height > 500:
height = 500
content = odoo.tools.image_resize_image(base64_source=content, size=(width or None, height or None),
encoding='base64', filetype='PNG', upper_limit=upper_limit,
avoid_if_small=avoid_if_small)
# resize force png as filetype
headers = self.force_contenttype(headers, contenttype='image/png')
+14 -2
View File
@@ -225,8 +225,20 @@
}
.o_attachment_image > img {
width: 100%;
height: auto;
overflow: hidden;
padding-top: 75%;
.o_attachment_image {
@include o-position-absolute(0, 0, 0, 0);
background-size: cover;
background-position: center;
transform: scale(1.05);
transition: all 0.3s;
}
&:hover .o_attachment_image {
transform: scale(1);
}
}
.o_progressbar {
+3 -2
View File
@@ -235,7 +235,7 @@ class Http(models.AbstractModel):
def binary_content(cls, xmlid=None, model='ir.attachment', id=None, field='datas',
unique=False, filename=None, filename_field='datas_fname', download=False,
mimetype=None, default_mimetype='application/octet-stream',
access_token=None, env=None):
access_token=None, share_id=None, share_token=None, force_ext=False, env=None):
env = env or request.env
obj = None
if xmlid:
@@ -248,7 +248,8 @@ class Http(models.AbstractModel):
return super(Http, cls).binary_content(
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
filename_field=filename_field, download=download, mimetype=mimetype,
default_mimetype=default_mimetype, access_token=access_token, env=env)
default_mimetype=default_mimetype, access_token=access_token, share_id=share_id, share_token=share_token,
force_ext=force_ext, env=env)
class ModelConverter(ModelConverter):
+57 -4
View File
@@ -6,14 +6,16 @@ import itertools
import logging
import mimetypes
import os
import io
import re
from collections import defaultdict
import uuid
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo import api, fields, models, tools, SUPERUSER_ID, exceptions, _
from odoo.exceptions import AccessError, ValidationError
from odoo.tools import config, human_size, ustr, html_escape
from odoo.tools.mimetypes import guess_mimetype
from odoo.tools import crop_image, image_resize_image
_logger = logging.getLogger(__name__)
@@ -42,6 +44,14 @@ class IrAttachment(models.Model):
record = self.env[attachment.res_model].browse(attachment.res_id)
attachment.res_name = record.display_name
@api.depends('res_model')
def _compute_res_model_name(self):
for record in self:
if record.res_model:
model = self.env['ir.model'].search([('model', '=', record.res_model)], limit=1)
if model:
record.res_model_name = model[0].name
@api.model
def _storage(self):
return self.env['ir.config_parameter'].sudo().get_param('ir_attachment.location', 'file')
@@ -272,15 +282,16 @@ class IrAttachment(models.Model):
"""
return ['base.group_system']
name = fields.Char('Attachment Name', required=True)
datas_fname = fields.Char('File Name')
name = fields.Char('Name', required=True)
datas_fname = fields.Char('Filename')
description = fields.Text('Description')
res_name = fields.Char('Resource Name', compute='_compute_res_name', store=True)
res_model = fields.Char('Resource Model', readonly=True, help="The database object this attachment will be attached to.")
res_model_name = fields.Char(compute='_compute_res_model_name', store=True, index=True)
res_field = fields.Char('Resource Field', readonly=True)
res_id = fields.Integer('Resource ID', readonly=True, help="The record id this is attached to.")
create_date = fields.Datetime('Date Created', readonly=True)
create_uid = fields.Many2one('res.users', string='Owner', readonly=True)
create_uid = fields.Many2one('res.users', readonly=True)
company_id = fields.Many2one('res.company', string='Company', change_default=True,
default=lambda self: self.env['res.company']._company_default_get('ir.attachment'))
type = fields.Selection([('url', 'URL'), ('binary', 'File')],
@@ -300,6 +311,18 @@ class IrAttachment(models.Model):
checksum = fields.Char("Checksum/SHA1", size=40, index=True, readonly=True)
mimetype = fields.Char('Mime Type', readonly=True)
index_content = fields.Text('Indexed Content', readonly=True, prefetch=False)
active = fields.Boolean(default=True, string="Active", oldname='archived')
thumbnail = fields.Binary(compute='_get_thumbnail', store=True, string="")
@api.depends('mimetype', 'file_size', 'checksum')
def _get_thumbnail(self):
for record in self:
if record.mimetype:
if re.match('image.*(gif|jpeg|jpg|png)', record.mimetype):
if not record.thumbnail:
temp_image = crop_image(record.datas, type='center', size=(100, 100), ratio=(1, 1))
record.thumbnail = image_resize_image(base64_source=temp_image, size=(100, 100),
encoding='base64', filetype='PNG')
@api.model_cr_context
def _auto_init(self):
@@ -438,6 +461,8 @@ class IrAttachment(models.Model):
@api.multi
def unlink(self):
if not self:
return True
self.check('unlink')
# First delete in the database, *then* in the filesystem if the
@@ -472,3 +497,31 @@ class IrAttachment(models.Model):
@api.model
def action_get(self):
return self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')
@api.model
def upload_attachment(self, attachments, custom_values=None):
"""
directly uploads attachments.
:param attachments: array of dictionaries representing attachment data:
eg: [{'name': 'doc.zip', 'data': 'data:application/zip;base64,R0lGODdhAQBADs='}]
:param custom_values: additional values for the attachment create dictionary.
:return: the ids of the new attachments.
"""
if not custom_values:
custom_values = {}
ids = []
for attachment in attachments:
data = attachment['data']
raw_name = attachment['name']
values = {
'mimetype': data[data.find(':') + 1:data.find(';')],
'name': raw_name[:raw_name.rfind('.')],
'datas': data[data.find(',') + 1:],
'datas_fname': raw_name,
}
values.update(custom_values)
created_attachment = self.env['ir.attachment'].create(values)
ids.append(created_attachment.id)
return ids
+32 -1
View File
@@ -23,6 +23,7 @@ from odoo.exceptions import AccessDenied, AccessError
from odoo.http import request, STATIC_CACHE, content_disposition
from odoo.tools import pycompat, consteq
from odoo.tools.mimetypes import guess_mimetype
from ast import literal_eval
from odoo.modules.module import get_resource_path, get_module_path
_logger = logging.getLogger(__name__)
@@ -246,7 +247,7 @@ class IrHttp(models.AbstractModel):
def binary_content(cls, xmlid=None, model='ir.attachment', id=None, field='datas',
unique=False, filename=None, filename_field='datas_fname', download=False,
mimetype=None, default_mimetype='application/octet-stream',
access_token=None, env=None):
access_token=None, share_id=None, share_token=None, force_ext=False, env=None):
""" Get file, attachment or downloadable content
If the ``xmlid`` and ``id`` parameter is omitted, fetches the default value for the
@@ -262,9 +263,13 @@ class IrHttp(models.AbstractModel):
:param str filename_field: if not create an filename with model-id-field
:param bool download: apply headers to download the file
:param str mimetype: mintype of the field (for headers)
:param share_id: the id of the documents.share that contains the attachment
:param share_token: the token of the documents.share that contains the attachment
:param str default_mimetype: default mintype if no mintype found
:param str access_token: optional token for unauthenticated access
only available for ir.attachment
:param bool force_ext: if true, adds the extension to the filename
that corresponds to the mimetype
:param Environment env: by default use request.env
:returns: (status, headers, content)
"""
@@ -277,6 +282,24 @@ class IrHttp(models.AbstractModel):
obj = env[model].sudo().browse(int(id))
if not consteq(obj.access_token, access_token):
return (403, [], None)
elif id and share_id and share_token:
share = env['documents.share'].sudo().browse(int(share_id))
if share:
if share.state == 'expired':
return (403, [], None)
if not consteq(share.access_token, share_token):
return (403, [], None)
elif share.type == 'ids' and (id in share.attachment_ids.ids):
obj = env[model].sudo().browse(int(id))
elif share.type == 'domain':
obj = env[model].sudo().browse(int(id))
share_domain = []
if share.domain:
share_domain = literal_eval(share.domain)
domain = [['folder_id', '=', share.folder_id.id]] + share_domain
attachments_check = http.request.env['ir.attachment'].sudo().search(domain)
if obj not in attachments_check:
return (403, [], None)
elif id and model in env.registry:
obj = env[model].browse(int(id))
@@ -338,6 +361,14 @@ class IrHttp(models.AbstractModel):
if not mimetype:
mimetype = guess_mimetype(base64.b64decode(content), default=default_mimetype)
if force_ext and (mimetype != default_mimetype):
dot_index = filename.rfind('.')
if dot_index > -1:
if mimetypes.guess_extension(mimetype) != filename[dot_index:]:
filename = filename[:dot_index] + mimetypes.guess_extension(mimetype)
else:
filename = filename + mimetypes.guess_extension(mimetype)
headers += [('Content-Type', mimetype), ('X-Content-Type-Options', 'nosniff')]
# cache
+81
View File
@@ -79,3 +79,84 @@ class test_ir_http_mimetype(common.TransactionCase):
)
mimetype = dict(headers).get('Content-Type')
self.assertEqual(mimetype, 'image/gif')
def test_ir_http_binary_content_force_ext_for_wrong_ext(self):
""" Test force mimetype for wrong extension when renamed """
attachment = self.env['ir.attachment'].create({
'datas': GIF,
'name': 'Test mimetype gif',
'datas_fname': 'file.gif'})
attachment.datas_fname = 'file.png'
status, headers, content = self.env['ir.http'].binary_content(
id=attachment.id,
mimetype=None,
default_mimetype='application/octet-stream',
env=self.env,
download=True,
force_ext=True,
)
disposition = dict(headers).get('Content-Disposition')
self.assertTrue(disposition.endswith('file.gif'))
def test_ir_http_binary_content_force_ext_for_right_ext(self):
""" Test force mimetype for right extension when renamed """
attachment = self.env['ir.attachment'].create({
'datas': GIF,
'name': 'Test mimetype gif',
'datas_fname': 'file.gif'})
attachment.datas_fname = 'file.gif'
status, headers, content = self.env['ir.http'].binary_content(
id=attachment.id,
mimetype=None,
default_mimetype='application/octet-stream',
env=self.env,
download=True,
force_ext=True,
)
disposition = dict(headers).get('Content-Disposition')
self.assertTrue(disposition.endswith('file.gif'))
def test_ir_http_binary_content_force_ext_without_ext(self):
""" Test force mimetype for right extension when renamed whith filename without extension """
attachment = self.env['ir.attachment'].create({
'datas': GIF,
'name': 'Test mimetype gif',
'datas_fname': 'file.gif'})
attachment.datas_fname = 'file'
status, headers, content = self.env['ir.http'].binary_content(
id=attachment.id,
mimetype=None,
default_mimetype='application/octet-stream',
env=self.env,
download=True,
force_ext=True,
)
disposition = dict(headers).get('Content-Disposition')
self.assertTrue(disposition.endswith('file.gif'))
def test_ir_http_binary_content_force_ext_for_binary_mimetype(self):
""" Test force mimetype for wrong extension when renamed and mimetyp is 'application/octet-stream' """
attachment = self.env['ir.attachment'].create({
'datas': GIF,
'name': 'Test mimetype exe',
'datas_fname': 'file.exe',
})
attachment.mimetype = 'application/octet-stream'
attachment.datas_fname = 'file.txt'
status, headers, content = self.env['ir.http'].binary_content(
id=attachment.id,
mimetype=None,
default_mimetype='application/octet-stream',
env=self.env,
download=True,
force_ext=True,
)
disposition = dict(headers).get('Content-Disposition')
self.assertTrue(disposition.endswith('file.txt'))
+23 -5
View File
@@ -18,7 +18,7 @@ Image._initialized = 2
# Image resizing
# ----------------------------------------
def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', filetype=None, avoid_if_small=False):
def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', filetype=None, avoid_if_small=False, upper_limit=False):
""" Function to resize an image. The image will be resized to the given
size, while keeping the aspect ratios, and holes in the image will be
filled with transparent background. The image will not be stretched if
@@ -63,18 +63,32 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
}.get(filetype, filetype)
asked_width, asked_height = size
if upper_limit:
if asked_width:
if asked_width >= image.size[0]:
asked_width = image.size[0]
if asked_height:
if asked_height >= image.size[1]:
asked_height = image.size[1]
if image.size[0] >= image.size[1]:
asked_height = None
else:
asked_width = None
if asked_width is None and asked_height is None:
return base64_source
if asked_width is None:
asked_width = int(image.size[0] * (float(asked_height) / image.size[1]))
if asked_height is None:
asked_height = int(image.size[1] * (float(asked_width) / image.size[0]))
size = asked_width, asked_height
# check image size: do not create a thumbnail if avoiding smaller images
if avoid_if_small and image.size[0] <= size[0] and image.size[1] <= size[1]:
return base64_source
if image.size != size:
image = image_resize_and_sharpen(image, size)
image = image_resize_and_sharpen(image, size, upper_limit=upper_limit)
if image.mode not in ["1", "L", "P", "RGB", "RGBA"] or (filetype == 'JPEG' and image.mode == 'RGBA'):
image = image.convert("RGB")
@@ -82,7 +96,7 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
image.save(background_stream, filetype)
return codecs.encode(background_stream.getvalue(), encoding)
def image_resize_and_sharpen(image, size, preserve_aspect_ratio=False, factor=2.0):
def image_resize_and_sharpen(image, size, preserve_aspect_ratio=False, factor=2.0, upper_limit=False):
"""
Create a thumbnail by resizing while keeping ratio.
A sharpen filter is applied for a better looking result.
@@ -101,8 +115,12 @@ def image_resize_and_sharpen(image, size, preserve_aspect_ratio=False, factor=2.
sharpener = ImageEnhance.Sharpness(image)
resized_image = sharpener.enhance(factor)
# create a transparent image for background and paste the image on it
image = Image.new('RGBA', size, (255, 255, 255, 0))
if upper_limit:
image = Image.new('RGBA', (size[0], size[1]-3), (255, 255, 255, 0)) # FIXME temporary fix for trimming the ghost border.
else:
image = Image.new('RGBA', size, (255, 255, 255, 0))
image.paste(resized_image, ((size[0] - resized_image.size[0]) // 2, (size[1] - resized_image.size[1]) // 2))
if image.mode != origin_mode:
image = image.convert(origin_mode)
return image