db215806cdca9ed047fd0bee8a0ce525d161304e
[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 try:
23     import cStringIO as StringIO
24 except ImportError:
25     import StringIO
26
27 from PIL import Image
28 from PIL import ImageEnhance
29 from random import randint
30
31 # ----------------------------------------
32 # Image resizing
33 # ----------------------------------------
34
35 def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', filetype=None, avoid_if_small=False):
36     """ Function to resize an image. The image will be resized to the given
37         size, while keeping the aspect ratios, and holes in the image will be
38         filled with transparent background. The image will not be stretched if
39         smaller than the expected size.
40         Steps of the resizing:
41         - Compute width and height if not specified.
42         - if avoid_if_small: if both image sizes are smaller than the requested
43           sizes, the original image is returned. This is used to avoid adding
44           transparent content around images that we do not want to alter but
45           just resize if too big. This is used for example when storing images
46           in the 'image' field: we keep the original image, resized to a maximal
47           size, without adding transparent content around it if smaller.
48         - create a thumbnail of the source image through using the thumbnail
49           function. Aspect ratios are preserved when using it. Note that if the
50           source image is smaller than the expected size, it will not be
51           extended, but filled to match the size.
52         - create a transparent background that will hold the final image.
53         - paste the thumbnail on the transparent background and center it.
54
55         :param base64_source: base64-encoded version of the source
56             image; if False, returns False
57         :param size: 2-tuple(width, height). A None value for any of width or
58             height mean an automatically computed value based respectivelly
59             on height or width of the source image.
60         :param encoding: the output encoding
61         :param filetype: the output filetype, by default the source image's
62         :type filetype: str, any PIL image format (supported for creation)
63         :param avoid_if_small: do not resize if image height and width
64             are smaller than the expected size.
65     """
66     if not base64_source:
67         return False
68     if size == (None, None):
69         return base64_source
70     image_stream = StringIO.StringIO(base64_source.decode(encoding))
71     image = Image.open(image_stream)
72     # store filetype here, as Image.new below will lose image.format
73     filetype = (filetype or image.format).upper()
74
75     filetype = {
76         'BMP': 'PNG',
77     }.get(filetype, filetype)
78
79     asked_width, asked_height = size
80     if asked_width is None:
81         asked_width = int(image.size[0] * (float(asked_height) / image.size[1]))
82     if asked_height is None:
83         asked_height = int(image.size[1] * (float(asked_width) / image.size[0]))
84     size = asked_width, asked_height
85
86     # check image size: do not create a thumbnail if avoiding smaller images
87     if avoid_if_small and image.size[0] <= size[0] and image.size[1] <= size[1]:
88         return base64_source
89
90     if image.size != size:
91         image = image_resize_and_sharpen(image, size)
92     if image.mode not in ["1", "L", "P", "RGB", "RGBA"]:
93         image = image.convert("RGB")
94
95     background_stream = StringIO.StringIO()
96     image.save(background_stream, filetype)
97     return background_stream.getvalue().encode(encoding)
98
99 def image_resize_and_sharpen(image, size, factor=2.0):
100     """
101         Create a thumbnail by resizing while keeping ratio.
102         A sharpen filter is applied for a better looking result.
103
104         :param image: PIL.Image.Image()
105         :param size: 2-tuple(width, height)
106         :param factor: Sharpen factor (default: 2.0)
107     """
108     if image.mode != 'RGBA':
109         image = image.convert('RGBA')
110     image.thumbnail(size, Image.ANTIALIAS)
111     sharpener = ImageEnhance.Sharpness(image)
112     resized_image = sharpener.enhance(factor)
113     # create a transparent image for background and paste the image on it
114     image = Image.new('RGBA', size, (255, 255, 255, 0))
115     image.paste(resized_image, ((size[0] - resized_image.size[0]) / 2, (size[1] - resized_image.size[1]) / 2))
116     return image
117
118 def image_save_for_web(image, fp=None, format=None):
119     """
120         Save image optimized for web usage.
121
122         :param image: PIL.Image.Image()
123         :param fp: File name or file object. If not specified, a bytestring is returned.
124         :param format: File format if could not be deduced from image.
125     """
126     opt = dict(format=image.format or format)
127     if image.format == 'PNG':
128         opt.update(optimize=True)
129         if image.mode != 'P':
130             # Floyd Steinberg dithering by default
131             image = image.convert('RGBA').convert('P', palette=Image.WEB, colors=256)
132     elif image.format == 'JPEG':
133         opt.update(optimize=True, quality=80)
134     if fp:
135         image.save(fp, **opt)
136     else:
137         img = StringIO.StringIO()
138         image.save(img, **opt)
139         return img.getvalue()
140
141 def image_resize_image_big(base64_source, size=(1204, 1024), encoding='base64', filetype=None, avoid_if_small=True):
142     """ Wrapper on image_resize_image, to resize images larger than the standard
143         'big' image size: 1024x1024px.
144         :param size, encoding, filetype, avoid_if_small: refer to image_resize_image
145     """
146     return image_resize_image(base64_source, size, encoding, filetype, avoid_if_small)
147
148 def image_resize_image_medium(base64_source, size=(128, 128), encoding='base64', filetype=None, avoid_if_small=False):
149     """ Wrapper on image_resize_image, to resize to the standard 'medium'
150         image size: 180x180.
151         :param size, encoding, filetype, avoid_if_small: refer to image_resize_image
152     """
153     return image_resize_image(base64_source, size, encoding, filetype, avoid_if_small)
154
155 def image_resize_image_small(base64_source, size=(64, 64), encoding='base64', filetype=None, avoid_if_small=False):
156     """ Wrapper on image_resize_image, to resize to the standard 'small' image
157         size: 50x50.
158         :param size, encoding, filetype, avoid_if_small: refer to image_resize_image
159     """
160     return image_resize_image(base64_source, size, encoding, filetype, avoid_if_small)
161
162 # ----------------------------------------
163 # Colors
164 # ---------------------------------------
165
166 def image_colorize(original, randomize=True, color=(255, 255, 255)):
167     """ Add a color to the transparent background of an image.
168         :param original: file object on the original image file
169         :param randomize: randomize the background color
170         :param color: background-color, if not randomize
171     """
172     # create a new image, based on the original one
173     original = Image.open(StringIO.StringIO(original))
174     image = Image.new('RGB', original.size)
175     # generate the background color, past it as background
176     if randomize:
177         color = (randint(32, 224), randint(32, 224), randint(32, 224))
178     image.paste(color)
179     image.paste(original, mask=original)
180     # return the new image
181     buffer = StringIO.StringIO()
182     image.save(buffer, 'PNG')
183     return buffer.getvalue()
184
185 # ----------------------------------------
186 # Misc image tools
187 # ---------------------------------------
188
189 def image_get_resized_images(base64_source, return_big=False, return_medium=True, return_small=True,
190     big_name='image', medium_name='image_medium', small_name='image_small',
191     avoid_resize_big=True, avoid_resize_medium=False, avoid_resize_small=False):
192     """ Standard tool function that returns a dictionary containing the
193         big, medium and small versions of the source image. This function
194         is meant to be used for the methods of functional fields for
195         models using images.
196
197         Default parameters are given to be used for the getter of functional
198         image fields,  for example with res.users or res.partner. It returns
199         only image_medium and image_small values, to update those fields.
200
201         :param base64_source: base64-encoded version of the source
202             image; if False, all returnes values will be False
203         :param return_{..}: if set, computes and return the related resizing
204             of the image
205         :param {..}_name: key of the resized image in the return dictionary;
206             'image', 'image_medium' and 'image_small' by default.
207         :param avoid_resize_[..]: see avoid_if_small parameter
208         :return return_dict: dictionary with resized images, depending on
209             previous parameters.
210     """
211     return_dict = dict()
212     if return_big:
213         return_dict[big_name] = image_resize_image_big(base64_source, avoid_if_small=avoid_resize_big)
214     if return_medium:
215         return_dict[medium_name] = image_resize_image_medium(base64_source, avoid_if_small=avoid_resize_medium)
216     if return_small:
217         return_dict[small_name] = image_resize_image_small(base64_source, avoid_if_small=avoid_resize_small)
218     return return_dict
219
220
221 if __name__=="__main__":
222     import sys
223
224     assert len(sys.argv)==3, 'Usage to Test: image.py SRC.png DEST.png'
225
226     img = file(sys.argv[1],'rb').read().encode('base64')
227     new = image_resize_image(img, (128,100))
228     file(sys.argv[2], 'wb').write(new.decode('base64'))
229