Thibault Delavallée (OpenERP) has proposed merging 
lp:~openerp-dev/openobject-server/trunk-image-standardize-tde into 
lp:openobject-server.

Requested reviews:
  OpenERP Core Team (openerp)

For more details, see:
https://code.launchpad.net/~openerp-dev/openobject-server/trunk-image-standardize-tde/+merge/118383

MyNotChatterTask: image resize.

This revision adds standard image field names and sizes (image: 1024x1024; 
image_medium: 180x180; image_small: 50x50, the last two being function fields). 
This server-side branch holds the tools functions for image resizing (with 
aspect ratio preserved), and update for res.partner and res.users.
-- 
https://code.launchpad.net/~openerp-dev/openobject-server/trunk-image-standardize-tde/+merge/118383
Your team OpenERP R&D Team is subscribed to branch 
lp:~openerp-dev/openobject-server/trunk-image-standardize-tde.
=== modified file 'openerp/addons/base/res/res_company.py'
--- openerp/addons/base/res/res_company.py	2012-07-13 15:35:20 +0000
+++ openerp/addons/base/res/res_company.py	2012-08-06 15:58:44 +0000
@@ -114,7 +114,7 @@
         'rml_header': fields.text('RML Header', required=True),
         'rml_header2': fields.text('RML Internal Header', required=True),
         'rml_header3': fields.text('RML Internal Header for Landscape Reports', required=True),
-        'logo': fields.related('partner_id', 'photo', string="Logo", type="binary"),
+        'logo': fields.related('partner_id', 'image', string="Logo", type="binary"),
         'currency_id': fields.many2one('res.currency', 'Currency', required=True),
         'currency_ids': fields.one2many('res.currency', 'company_id', 'Currency'),
         'user_ids': fields.many2many('res.users', 'res_company_users_rel', 'cid', 'user_id', 'Accepted Users'),

=== modified file 'openerp/addons/base/res/res_partner.py'
--- openerp/addons/base/res/res_partner.py	2012-08-02 13:46:48 +0000
+++ openerp/addons/base/res/res_partner.py	2012-08-06 15:58:44 +0000
@@ -133,6 +133,19 @@
             res[partner.id] =self._display_address(cr, uid, partner, context=context)
         return res
 
+    def _get_image(self, cr, uid, ids, name, args, context=None):
+        result = dict.fromkeys(ids, False)
+        for obj in self.browse(cr, uid, ids, context=context):
+            resized_image_dict = tools.get_resized_images(obj.image)
+            result[obj.id] = {
+                'image_medium': resized_image_dict['image_medium'],
+                'image_small': resized_image_dict['image_small'],
+                }
+        return result
+    
+    def _set_image(self, cr, uid, id, name, value, args, context=None):
+        return self.write(cr, uid, [id], {'image': tools.resize_image_big(value)}, context=context)
+
     _order = "name"
     _columns = {
         'name': fields.char('Name', size=128, required=True, select=True),
@@ -174,7 +187,26 @@
         'birthdate': fields.char('Birthdate', size=64),
         'is_company': fields.boolean('Company', help="Check if the contact is a company, otherwise it is a person"),
         'use_parent_address': fields.boolean('Use Company Address', help="Select this if you want to set company's address information  for this contact"),
-        'photo': fields.binary('Photo'),
+        'image': fields.binary("Image",
+            help="This field holds the image used as avatar for the "\
+                 "partner. The image is base64 encoded, and PIL-supported. "\
+                 "It is limited to a 1024x1024 px image."),
+        'image_medium': fields.function(_get_image, fnct_inv=_set_image,
+            string="Medium-sized image", type="binary", multi="_get_image",
+            store = {
+                'res.partner': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
+            },
+            help="Medium-sized image of the partner. It is automatically "\
+                 "resized as a 180x180 px image, with aspect ratio preserved. "\
+                 "Use this field in form views or some kanban views."),
+        'image_small': fields.function(_get_image, fnct_inv=_set_image,
+            string="Small-sized image", type="binary", multi="_get_image",
+            store = {
+                'res.partner': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
+            },
+            help="Small-sized image of the partner. It is automatically "\
+                 "resized as a 50x50 px image, with aspect ratio preserved. "\
+                 "Use this field anywhere a small image is required."),
         'company_id': fields.many2one('res.company', 'Company', select=1),
         'color': fields.integer('Color Index'),
         'contact_address': fields.function(_address_display,  type='char', string='Complete Address'),
@@ -187,12 +219,12 @@
             return [context['category_id']]
         return False
 
-    def _get_photo(self, cr, uid, is_company, context=None):
+    def _get_default_image(self, cr, uid, is_company, context=None):
         if is_company:
-            path = os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'company_icon.png')
+            image_path = os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'company_icon.png')
         else:
-            path = os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'photo.png')
-        return open(path, 'rb').read().encode('base64')
+            image_path = os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'photo.png')
+        return tools.resize_image_big(open(image_path, 'rb').read().encode('base64'))
 
     _defaults = {
         'active': True,
@@ -203,7 +235,7 @@
         'is_company': False,
         'type': 'default',
         'use_parent_address': True,
-        'photo': lambda self, cr, uid, context: self._get_photo(cr, uid, False, context),
+        'image': lambda self, cr, uid, context: self._get_default_image(cr, uid, False, context),
     }
 
     def copy(self, cr, uid, id, default=None, context=None):
@@ -214,8 +246,9 @@
         return super(res_partner, self).copy(cr, uid, id, default, context)
 
     def onchange_type(self, cr, uid, ids, is_company, context=None):
-        value = {'title': False,
-                 'photo': self._get_photo(cr, uid, is_company, context)}
+        # get value as for an onchange on the image
+        value = tools.get_resized_images(self._get_default_image(cr, uid, is_company, context))
+        value['title'] = False
         if is_company:
             value['parent_id'] = False
             domain = {'title': [('domain', '=', 'partner')]}
@@ -280,8 +313,9 @@
             domain_siblings = [('parent_id', '=', vals['parent_id']), ('use_parent_address', '=', True)]
             update_ids = [vals['parent_id']] + self.search(cr, uid, domain_siblings, context=context)
             self.update_address(cr, uid, update_ids, vals, context)
-        if 'photo' not in vals  :
-            vals['photo'] = self._get_photo(cr, uid, vals.get('is_company', False) or context.get('default_is_company'), context)
+        if 'image' not in vals :
+            image_value = self._get_default_image(cr, uid, vals.get('is_company', False) or context.get('default_is_company'), context)
+            vals.update(tools.get_resized_images(image_value))
         return super(res_partner,self).create(cr, uid, vals, context=context)
 
     def update_address(self, cr, uid, ids, vals, context=None):

=== modified file 'openerp/addons/base/res/res_partner_view.xml'
--- openerp/addons/base/res/res_partner_view.xml	2012-08-04 15:06:39 +0000
+++ openerp/addons/base/res/res_partner_view.xml	2012-08-06 15:58:44 +0000
@@ -99,7 +99,7 @@
             <field name="arch" type="xml">
                 <form string="Partners" version="7.0">
                 <sheet>
-                    <field name="photo" widget='image' class="oe_avatar oe_left"/>
+                    <field name="image_small" widget='image' class="oe_avatar oe_left"/>
                     <div class="oe_title">
                         <h1>
                             <field name="name" default_focus="1" placeholder="Name" />
@@ -166,7 +166,7 @@
                                     <field name="phone"/>
                                     <field name="street"/>
                                     <field name="street2"/>
-                                    <field name="photo"/>
+                                    <field name="image_small"/>
                                     <field name="zip"/>
                                     <field name="city"/>
                                     <field name="country_id"/>
@@ -179,7 +179,7 @@
                                                 <a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a>
                                                 <div class="oe_module_vignette">
                                                 <a type="edit">
-                                                    <img t-att-src="kanban_image('res.partner', 'photo', record.id.value)" class="oe_avatar oe_kanban_avatar_toto"/>
+                                                    <img t-att-src="kanban_image('res.partner', 'image_small', record.id.value)" class="oe_avatar oe_kanban_avatar_toto"/>
                                                 </a>
                                                     <div class="oe_module_desc">
                                                         <div class="oe_kanban_box_content oe_kanban_color_bglight oe_kanban_box_show_onclick_trigger oe_kanban_color_border">
@@ -284,7 +284,7 @@
                     <field name="phone"/>
                     <field name="street"/>
                     <field name="street2"/>
-                    <field name="photo"/>
+                    <field name="image_small"/>
                     <field name="zip"/>
                     <field name="city"/>
                     <field name="country_id"/>
@@ -295,7 +295,7 @@
                         <t t-name="kanban-box">
                             <div class="oe_kanban_vignette">
                                 <a type="edit">
-                                    <img t-att-src="kanban_image('res.partner', 'photo', record.id.value)" class="oe_kanban_image"/>
+                                    <img t-att-src="kanban_image('res.partner', 'image_small', record.id.value)" class="oe_kanban_image"/>
                                 </a>
                                 <div class="oe_kanban_details">
                                     <h4 class="oe_partner_heading"><a type="edit"><field name="name"/></a></h4>

=== modified file 'openerp/addons/base/res/res_users.py'
--- openerp/addons/base/res/res_users.py	2012-07-17 06:44:10 +0000
+++ openerp/addons/base/res/res_users.py	2012-08-06 15:58:44 +0000
@@ -25,7 +25,6 @@
 
 import pytz
 
-import io, StringIO
 from lxml import etree
 from lxml.builder import E
 import netsvc
@@ -33,7 +32,6 @@
 import openerp.exceptions
 from osv import fields,osv
 from osv.orm import browse_record
-from PIL import Image
 import pooler
 import random
 from service import security
@@ -152,33 +150,6 @@
                                          body=(self.get_welcome_mail_body(cr, uid, context=context) % user))
         return ir_mail_server.send_email(cr, uid, msg, context=context)
 
-    def onchange_avatar(self, cr, uid, ids, value, context=None):
-        if not value:
-            return {'value': {'avatar_big': value, 'avatar': value} }
-        return {'value': {'avatar_big': self._avatar_resize(cr, uid, value, 540, 450, context=context), 'avatar': self._avatar_resize(cr, uid, value, context=context)} }
-    
-    def _set_avatar(self, cr, uid, id, name, value, args, context=None):
-        if not value:
-            vals = {'avatar_big': value}
-        else:
-            vals = {'avatar_big': self._avatar_resize(cr, uid, value, 540, 450, context=context)}
-        return self.write(cr, uid, [id], vals, context=context)
-    
-    def _avatar_resize(self, cr, uid, avatar, height=180, width=150, context=None):
-        image_stream = io.BytesIO(avatar.decode('base64'))
-        img = Image.open(image_stream)
-        img.thumbnail((height, width), Image.ANTIALIAS)
-        img_stream = StringIO.StringIO()
-        img.save(img_stream, "PNG")
-        return img_stream.getvalue().encode('base64')
-
-    def _get_avatar(self, cr, uid, ids, name, args, context=None):
-        result = dict.fromkeys(ids, False)
-        for user in self.browse(cr, uid, ids, context=context):
-            if user.avatar_big:
-                result[user.id] = self._avatar_resize(cr, uid, user.avatar_big, context=context)
-        return result
-
     def _set_new_password(self, cr, uid, id, name, value, args, context=None):
         if value is False:
             # Do not update the password if no value is provided, ignore silently.
@@ -194,6 +165,19 @@
     def _get_password(self, cr, uid, ids, arg, karg, context=None):
         return dict.fromkeys(ids, '')
 
+    def _get_image(self, cr, uid, ids, name, args, context=None):
+        result = dict.fromkeys(ids, False)
+        for obj in self.browse(cr, uid, ids, context=context):
+            resized_image_dict = tools.get_resized_images(obj.image)
+            result[obj.id] = {
+                'image_medium': resized_image_dict['image_medium'],
+                'image_small': resized_image_dict['image_small'],
+                }
+        return result
+    
+    def _set_image(self, cr, uid, id, name, value, args, context=None):
+        return self.write(cr, uid, [id], {'image': tools.resize_image_big(value)}, context=context)
+    
     _columns = {
         'id': fields.integer('ID'),
         'name': fields.char('User Name', size=64, required=True, select=True,
@@ -208,11 +192,26 @@
                                                             "otherwise leave empty. After a change of password, the user has to login again."),
         'user_email': fields.char('Email', size=64),
         'signature': fields.text('Signature', size=64),
-        'avatar_big': fields.binary('Big-sized avatar', help="This field holds the image used as avatar for the user. The avatar field is used as an interface to access this field. The image is base64 encoded, and PIL-supported. It is stored as a 540x450 px image, in case a bigger image must be used."),
-        'avatar': fields.function(_get_avatar, fnct_inv=_set_avatar, string='Avatar', type="binary",
-            store = {
-                'res.users': (lambda self, cr, uid, ids, c={}: ids, ['avatar_big'], 10),
-            }, help="Image used as avatar for the user. It is automatically resized as a 180x150 px image. This field serves as an interface to the avatar_big field."),
+        'image': fields.binary("Avatar",
+            help="This field holds the image used as avatar for the "\
+                 "user. The image is base64 encoded, and PIL-supported. "\
+                 "It is limited to a 1024x1024 px image."),
+        'image_medium': fields.function(_get_image, fnct_inv=_set_image,
+            string="Medium-sized avatar", type="binary", multi="_get_image",
+            store = {
+                'res.users': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
+            },
+            help="Medium-sized image of the user. It is automatically "\
+                 "resized as a 180x180 px image, with aspect ratio preserved. "\
+                 "Use this field in form views or some kanban views."),
+        'image_small': fields.function(_get_image, fnct_inv=_set_image,
+            string="Smal-sized avatar", type="binary", multi="_get_image",
+            store = {
+                'res.users': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
+            },
+            help="Small-sized image of the user. It is automatically "\
+                 "resized as a 50x50 px image, with aspect ratio preserved. "\
+                 "Use this field anywhere a small image is required."),
         'active': fields.boolean('Active'),
         'action_id': fields.many2one('ir.actions.actions', 'Home Action', help="If specified, this action will be opened at logon for this user, in addition to the standard menu."),
         'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
@@ -320,16 +319,16 @@
             pass
         return result
 
-    def _get_avatar(self, cr, uid, context=None):
-        # default avatar file name: avatar0 -> avatar6.png, choose randomly
-        avatar_path = openerp.modules.get_module_resource('base', 'static/src/img', 'avatar%d.png' % random.randint(0, 6))
-        return self._avatar_resize(cr, uid, open(avatar_path, 'rb').read().encode('base64'), context=context)
+    def _get_default_image(self, cr, uid, context=None):
+        # default image file name: avatar0 -> avatar6.png, choose randomly
+        image_path = openerp.modules.get_module_resource('base', 'static/src/img', 'avatar%d.png' % random.randint(0, 6))
+        return tools.resize_image_big(open(image_path, 'rb').read().encode('base64'))
 
     _defaults = {
         'password' : '',
         'context_lang': lambda self, cr, uid, context: context.get('lang', 'en_US'),
         'context_tz': lambda self, cr, uid, context: context.get('tz', False),
-        'avatar': _get_avatar,
+        'image': _get_default_image,
         'active' : True,
         'menu_id': _get_menu,
         'company_id': _get_company,
@@ -338,7 +337,7 @@
     }
 
     # User can write to a few of her own fields (but not her groups for example)
-    SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'user_email', 'name', 'avatar', 'avatar_big']
+    SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'user_email', 'name', 'image', 'image_medium', 'image_small']
 
     def write(self, cr, uid, ids, values, context=None):
         if not hasattr(ids, '__iter__'):

=== modified file 'openerp/addons/base/res/res_users_view.xml'
--- openerp/addons/base/res/res_users_view.xml	2012-08-02 15:13:44 +0000
+++ openerp/addons/base/res/res_users_view.xml	2012-08-06 15:58:44 +0000
@@ -91,7 +91,7 @@
                 <form string="Users" version="7.0">
                     <field name="id" invisible="1"/>
                     <sheet>
-                        <field name="avatar" widget='image' on_change="onchange_avatar(avatar)" class="oe_avatar oe_left"/>
+                        <field name="image_medium" widget='image' class="oe_avatar oe_left"/>
                         <div class="oe_title">
                             <label for="name" class="oe_edit_only"/>
                             <h1><field name="name"/></h1>
@@ -203,7 +203,7 @@
                 <form string="Users" version="7.0">
                    <sheet>
                     <div class="oe_right oe_avatar">
-                        <field name="avatar" widget='image' on_change="onchange_avatar(avatar)"/>
+                        <field name="image_small" widget='image' class="oe_image_small"/>
                     </div>
                     <h1>
                         <field name="name" readonly="1" class="oe_inline"/>

=== modified file 'openerp/tools/__init__.py'
--- openerp/tools/__init__.py	2011-12-16 16:04:26 +0000
+++ openerp/tools/__init__.py	2012-08-06 15:58:44 +0000
@@ -19,19 +19,20 @@
 #
 ##############################################################################
 
-import copy
-import win32
+from amount_to_text import *
+from amount_to_text_en import *
 from config import config
-from misc import *
 from convert import *
-from translate import *
+import copy
+from float_utils import *
 from graph import graph
-from amount_to_text import *
-from amount_to_text_en import *
+from image import *
+from misc import *
 from pdf_utils import *
+from sql import *
+from translate import *
 from yaml_import import *
-from sql import *
-from float_utils import *
+import win32
 
 #.apidoc title: Tools
 

=== added file 'openerp/tools/image.py'
--- openerp/tools/image.py	1970-01-01 00:00:00 +0000
+++ openerp/tools/image.py	2012-08-06 15:58:44 +0000
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    Copyright (C) 2012-today OpenERP s.a. (<http://openerp.com>).
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import io
+from PIL import Image
+import StringIO
+
+# ----------------------------------------
+# Image resizing
+# ----------------------------------------
+
+def resize_image(base64_source, size=(1024, 1024), encoding='base64', filetype='PNG', avoid_if_small=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 smaller than the expected size.
+        Steps of the resizing:
+        - create a thumbnail of the source image through using the
+          thumbnail function. Aspect ratios are preserved when using
+          it. Note that if the source image is smaller than the expected
+          size, it will not be extended, but filled to match the size.
+        - create a transparent background that will hold the final
+          image.
+        - past the thumbnail on the transparent background and center
+          it.
+        
+        :param base64_source: base64-encoded version of the source
+                              image
+        :param size: tuple(height, width)
+        :param encoding: the output encoding
+        :param filetype: the output filetype
+        :param avoid_if_small: do not resize if image height and width
+                               are smaller than the expected size.
+    """
+    image_stream = io.BytesIO(base64_source.decode(encoding))
+    image = Image.open(image_stream)
+    # 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
+    # create a thumbnail: will resize and keep ratios
+    image.thumbnail(size, Image.ANTIALIAS)
+    # create a transparent image for background
+    background = Image.new('RGBA', size, (255, 255, 255, 0))
+    # past the resized image on the background
+    background.paste(image, ((size[0] - image.size[0]) / 2, (size[1] - image.size[1]) / 2))
+    # return an encoded image
+    background_stream = StringIO.StringIO()
+    background.save(background_stream, filetype)
+    return background_stream.getvalue().encode(encoding)
+
+def resize_image_big(base64_source, size=(1204, 1204), encoding='base64', filetype='PNG'):
+    """ Wrapper on resize_image, to resize to the standard 'big' image
+        size: 1024x1024.
+        :param base64_source: base64 encoded source image. If False,
+                              the function returns False.
+    """
+    if not base64_source:
+        return False
+    return resize_image(base64_source, size, encoding, filetype, True)
+
+def resize_image_medium(base64_source, size=(180, 180), encoding='base64', filetype='PNG'):
+    """ Wrapper on resize_image, to resize to the standard 'medium'
+        image size: 180x180.
+        :param base64_source: base64 encoded source image. If False,
+                              the function returns False.
+    """
+    if not base64_source:
+        return False
+    return resize_image(base64_source, size, encoding, filetype)
+    
+def resize_image_small(base64_source, size=(50, 50), encoding='base64', filetype='PNG'):
+    """ Wrapper on resize_image, to resize to the standard 'small' image
+        size: 50x50.
+        :param base64_source: base64 encoded source image. If False,
+                              the function returns False.
+    """
+    if not base64_source:
+        return False
+    return resize_image(base64_source, size, encoding, filetype)
+
+# ----------------------------------------
+# Misc image tools
+# ---------------------------------------
+
+def get_resized_images(base64_source, big_name='image', medium_name='image_medium', small_name='image_small'):
+    """ Standard tool function that returns a dictionary containing the
+        big, medium and small versions of the source image. This function
+        is meant to be used for the methods of functional fields for
+        models using images.
+        
+        :param base64_source: if set to False, other values are set to
+                              False also. The purpose is to be linked
+                              to the fields that hold images in
+                              OpenERP and that are binary fields.
+        :param big_name: name related to the big version of the image;
+                         'image' by default
+        :param medium_name: name related to the medium version of the
+                            image; 'image_medium' by default
+        :param small_name: name related to the small version of the
+                           image; 'image_small' by default
+    """
+    return {
+        big_name: resize_image_big(base64_source),
+        medium_name: resize_image_medium(base64_source),
+        small_name: resize_image_small(base64_source),
+        }

_______________________________________________
Mailing list: https://launchpad.net/~openerp-dev-gtk
Post to     : [email protected]
Unsubscribe : https://launchpad.net/~openerp-dev-gtk
More help   : https://help.launchpad.net/ListHelp

Reply via email to