Merge pull request #648 from odoo-dev/7.0-fix-searchbar-navigation-ged
[odoo/odoo.git] / openerp / tools / image.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2012-today OpenERP s.a. (<http://openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import io
23 import StringIO
24
25 from PIL import Image
26 from PIL import ImageEnhance
27 from random import random
28
29 # ----------------------------------------
30 # Image resizing
31 # ----------------------------------------
32
33 def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', filetype='PNG', avoid_if_small=False):
34     """ Function to resize an image. The image will be resized to the given
35         size, while keeping the aspect ratios, and holes in the image will be
36         filled with transparent background. The image will not be stretched if
37         smaller than the expected size.
38         Steps of the resizing:
39         - Compute width and height if not specified.
40         - if avoid_if_small: if both image sizes are smaller than the requested
41           sizes, the original image is returned. This is used to avoid adding
42           transparent content around images that we do not want to alter but
43           just resize if too big. This is used for example when storing images
44           in the 'image' field: we keep the original image, resized to a maximal
45           size, without adding transparent content around it if smaller.
46         - create a thumbnail of the source image through using the thumbnail
47           function. Aspect ratios are preserved when using it. Note that if the
48           source image is smaller than the expected size, it will not be
49           extended, but filled to match the size.
50         - create a transparent background that will hold the final image.
51         - paste the thumbnail on the transparent background and center it.
52
53         :param base64_source: base64-encoded version of the source
54             image; if False, returns False
55         :param size: 2-tuple(width, height). A None value for any of width or
56             height mean an automatically computed value based respectivelly
57             on height or width of the source image.
58         :param encoding: the output encoding
59         :param filetype: the output filetype
60         :param avoid_if_small: do not resize if image height and width
61             are smaller than the expected size.
62     """
63     if not base64_source:
64         return False
65     if size == (None, None):
66         return base64_source
67     image_stream = io.BytesIO(base64_source.decode(encoding))
68     image = Image.open(image_stream)
69
70     asked_width, asked_height = size
71     if asked_width is None:
72         asked_width = int(image.size[0] * (float(asked_height) / image.size[1]))
73     if asked_height is None:
74         asked_height = int(image.size[1] * (float(asked_width) / image.size[0]))
75     size = asked_width, asked_height
76
77     # check image size: do not create a thumbnail if avoiding smaller images
78     if avoid_if_small and image.size[0] <= size[0] and image.size[1] <= size[1]:
79         return base64_source
80
81     if image.size <> size:
82         # create a thumbnail: will resize and keep ratios, then sharpen for better looking result
83         image.thumbnail(size, Image.ANTIALIAS)
84         sharpener = ImageEnhance.Sharpness(image.convert('RGBA'))
85         resized_image = sharpener.enhance(2.0)
86         # create a transparent image for background and paste the image on it
87         image = Image.new('RGBA', size, (255, 255, 255, 0))
88         image.paste(resized_image, ((size[0] - resized_image.size[0]) / 2, (size[1] - resized_image.size[1]) / 2))
89     if image.mode not in ["1", "L", "P", "RGB", "RGBA"]:
90         image = image.convert("RGB")
91
92     background_stream = StringIO.StringIO()
93     image.save(background_stream, filetype)
94     return background_stream.getvalue().encode(encoding)
95
96 def image_resize_image_big(base64_source, size=(1204, 1204), encoding='base64', filetype='PNG', avoid_if_small=True):
97     """ Wrapper on image_resize_image, to resize images larger than the standard
98         'big' image size: 1024x1024px.
99         :param size, encoding, filetype, avoid_if_small: refer to image_resize_image
100     """
101     return image_resize_image(base64_source, size, encoding, filetype, avoid_if_small)
102
103 def image_resize_image_medium(base64_source, size=(128, 128), encoding='base64', filetype='PNG', avoid_if_small=False):
104     """ Wrapper on image_resize_image, to resize to the standard 'medium'
105         image size: 180x180.
106         :param size, encoding, filetype, avoid_if_small: refer to image_resize_image
107     """
108     return image_resize_image(base64_source, size, encoding, filetype, avoid_if_small)
109
110 def image_resize_image_small(base64_source, size=(64, 64), encoding='base64', filetype='PNG', avoid_if_small=False):
111     """ Wrapper on image_resize_image, to resize to the standard 'small' image
112         size: 50x50.
113         :param size, encoding, filetype, avoid_if_small: refer to image_resize_image
114     """
115     return image_resize_image(base64_source, size, encoding, filetype, avoid_if_small)
116
117 # ----------------------------------------
118 # Colors
119 # ---------------------------------------
120
121 def image_colorize(original, randomize=True, color=(255, 255, 255)):
122     """ Add a color to the transparent background of an image.
123         :param original: file object on the original image file
124         :param randomize: randomize the background color
125         :param color: background-color, if not randomize
126     """
127     # create a new image, based on the original one
128     original = Image.open(io.BytesIO(original))
129     image = Image.new('RGB', original.size)
130     # generate the background color, past it as background
131     if randomize:
132         color = (int(random() * 192 + 32), int(random() * 192 + 32), int(random() * 192 + 32))
133     image.paste(color)
134     image.paste(original, mask=original)
135     # return the new image
136     buffer = StringIO.StringIO()
137     image.save(buffer, 'PNG')
138     return buffer.getvalue()
139
140 # ----------------------------------------
141 # Misc image tools
142 # ---------------------------------------
143
144 def image_get_resized_images(base64_source, return_big=False, return_medium=True, return_small=True,
145     big_name='image', medium_name='image_medium', small_name='image_small',
146     avoid_resize_big=True, avoid_resize_medium=False, avoid_resize_small=False):
147     """ Standard tool function that returns a dictionary containing the
148         big, medium and small versions of the source image. This function
149         is meant to be used for the methods of functional fields for
150         models using images.
151
152         Default parameters are given to be used for the getter of functional
153         image fields,  for example with res.users or res.partner. It returns
154         only image_medium and image_small values, to update those fields.
155
156         :param base64_source: base64-encoded version of the source
157             image; if False, all returnes values will be False
158         :param return_{..}: if set, computes and return the related resizing
159             of the image
160         :param {..}_name: key of the resized image in the return dictionary;
161             'image', 'image_medium' and 'image_small' by default.
162         :param avoid_resize_[..]: see avoid_if_small parameter
163         :return return_dict: dictionary with resized images, depending on
164             previous parameters.
165     """
166     return_dict = dict()
167     if return_big:
168         return_dict[big_name] = image_resize_image_big(base64_source, avoid_if_small=avoid_resize_big)
169     if return_medium:
170         return_dict[medium_name] = image_resize_image_medium(base64_source, avoid_if_small=avoid_resize_medium)
171     if return_small:
172         return_dict[small_name] = image_resize_image_small(base64_source, avoid_if_small=avoid_resize_small)
173     return return_dict
174
175
176 if __name__=="__main__":
177     import sys
178
179     assert len(sys.argv)==3, 'Usage to Test: image.py SRC.png DEST.png'
180
181     img = file(sys.argv[1],'rb').read().encode('base64')
182     new = image_resize_image(img, (128,100))
183     file(sys.argv[2], 'wb').write(new.decode('base64'))
184