Source code for ImageMetaTag.savefig

'''
This module contains a wrapper for matplotlib.pyplot.savefig. The primary
function of the wrapper is to add image metadata taggging and database storage
of that metadata.

As the output images are already being post-processed to add the metadata,
basic image manipulation options are included to crop images, add logos and
reduce their file size by simplifying their colour palette.

.. moduleauthor:: Melissa Brooks https://github.com/melissaebrooks

(C) Crown copyright Met Office. All rights reserved.
Released under BSD 3-Clause License. See LICENSE for more details.

'''

import os
import sys
import io
import sqlite3
import pdb
from datetime import datetime
import matplotlib.pyplot as plt

from ImageMetaTag import db, META_IMG_FORMATS, RESERVED_TAGS
from ImageMetaTag import POSTPROC_IMG_FORMATS, DPI_IMG_FORMATS
from ImageMetaTag import DEFAULT_DB_TIMEOUT, DEFAULT_DB_ATTEMPTS

# image manipulations:
from PIL import Image, ImageChops, PngImagePlugin
import numpy as np

THUMB_DEFAULT_IMG_SIZE = 150, 150
THUMB_DEFAULT_DIR_NAME = 'thumbnail'

[docs]def savefig(filename, fig=None, img_tags=None, img_format=None, img_converter=0, do_trim=False, trim_border=0, do_thumb=False, keep_open=False, dpi=None, logo_file=None, logo_width=None, logo_height=None, logo_padding=0, logo_pos=0, db_file=None, db_timeout=DEFAULT_DB_TIMEOUT, db_attempts=DEFAULT_DB_ATTEMPTS, db_replace=False, db_add_strict=False, db_full_paths=False, verbose=False): ''' A wrapper around matplotlib.pyplot.savefig, to include file size optimisation and image tagging. The filesize optimisation depends on the img_converter input passes into :func:`ImageMetaTag.image_file_postproc`. Arguments: * filename (can include the file extension, or that can be specified in \ the img_format option) Options: * fig - matplotlib figure to save * img_format - file format of the image. If not supplied it will be \ guessed from the filename. Currently only the png file \ format is supported for tagging/conversion. * img_tags - a dictionary of {tag_name : value} pairs to be added to the\ image metadata. Both tag_name and value should be strings \ and if is not possible to save a tag_name that is used for \ system purposes of image readers/writers. (e.g. dpi, gamma, interlace, transparency etc.) * db_file - a database file to be used by \ :func:`ImageMetaTag.db.write_img_to_dbfile` to store all \ image metadata so they can be quickly accessed. * db_full_paths - by default, if the images can be expressed as relative \ path to the database file then the database will \ contain only relative links, unless db_full_paths is \ True. * db_timeout - change the database timeout (in seconds). * db_attempts - change the number of attempts to write to the database. * db_replace - if True, an image's metadata will be replaced in the \ database if it already exists. This can be slow, and the \ metadata is usually the same so the default is \ db_replace=False. * db_add_strict - if True, any attempt to add an image whose metadata \ tag_names are not present in a pre-existing database \ will result in a ValueError being raised. \ If False, then adding a new metadata tag to the \ database will cause it be rewritten with the new item \ as a new column. All pre-existing images will have \ the new tag set to 'None'. It is best to avoid using \ this functionality as it can be slow for large \ databases. Instead, all images should be ideally have \ all expected metadata tags included from the start \ but set to 'None' where they are not used. * dpi - change the image resolution passed into matplotlib.savefig. * keep_open - by default, this savefig wrapper closes the figure after \ use, except if keep_open is True. * verbose - switch for verbose output (reports file sizes before/after \ conversion) * img_converter - see :func:`ImageMetaTag.image_file_postproc`. * do_trim - see :func:`ImageMetaTag.image_file_postproc`. * trim_border - see :func:`ImageMetaTag.image_file_postproc`. * logo_file - see :func:`ImageMetaTag.image_file_postproc`. * logo_width - see :func:`ImageMetaTag.image_file_postproc`. * logo_height - see :func:`ImageMetaTag.image_file_postproc`. * logo_padding - see :func:`ImageMetaTag.image_file_postproc`. * logo_pos - see :func:`ImageMetaTag.image_file_postproc`. * do_thumb - see :func:`ImageMetaTag.image_file_postproc`. ''' # If fig given then use fig methods, else use pyplot methods if fig is not None: savefig_fn = fig.savefig close_fn = fig.clear else: savefig_fn = plt.savefig close_fn = plt.close if img_format is None: write_file = filename # get the img_format from the end of the filename _, img_format = os.path.splitext(filename) if img_format is None or img_format == '': msg = 'Cannot determine img_format to save from filename "{}"' raise ValueError(msg.format(filename)) # get rid of the . to be consistent throughout img_format = img_format[1:] else: if img_format.startswith('.'): img_format = img_format[1:] write_file = '%s.%s' % (filename, img_format) # Where to save the figure to? If we're going to postprocess it, save to # memory for speed and to cut down on IO load: do_any_postproc = (img_format in META_IMG_FORMATS or img_format in POSTPROC_IMG_FORMATS) if do_any_postproc: buf = io.BytesIO() savefig_file = buf else: savefig_file = write_file buf = None # should probably add lots of other args, or use **kwargs if dpi: savefig_fn(savefig_file, dpi=dpi) else: savefig_fn(savefig_file) if not keep_open: close_fn() if buf: # need to go to the start of the buffer, if that's where it went: buf.seek(0) if img_format in META_IMG_FORMATS: use_img_tags = img_tags else: use_img_tags = None if verbose: postproc_st = datetime.now() if img_format in POSTPROC_IMG_FORMATS: if dpi is None or img_format not in DPI_IMG_FORMATS: img_dpi = None else: img_dpi = (dpi, dpi) image_file_postproc(write_file, img_buf=buf, img_dpi=img_dpi, img_converter=img_converter, do_trim=do_trim, trim_border=trim_border, logo_file=logo_file, logo_width=logo_width, logo_height=logo_height, logo_padding=logo_padding, logo_pos=logo_pos, do_thumb=do_thumb, img_tags=use_img_tags, verbose=verbose) else: msg = 'Currently, ImageMetaTag does not support "{}" format images' raise NotImplementedError(msg.format(img_format)) # image post-processing completed, so close the buffer if we opened it: if buf: buf.close() if verbose: msg = 'Image post-processing took: {}' print(msg.format(str(datetime.now() - postproc_st))) # now write to the database, if it is specifed: if not (db_file is None or img_tags is None): if verbose: db_st = datetime.now() # if the image path can be expressed as a relative path compared # to the database file, then do so (unless told otherwise). db_dir = os.path.split(db_file)[0] if filename.startswith(db_dir) and not db_full_paths: db_filename = os.path.relpath(filename, db_dir) else: db_filename = filename wrote_db = False n_tries = 1 while not wrote_db and n_tries <= db_attempts: try: db.write_img_to_dbfile(db_file, db_filename, img_tags, timeout=db_timeout, attempt_replace=db_replace, add_strict=db_add_strict) wrote_db = True except sqlite3.OperationalError as op_err: if 'database is locked' in repr(op_err): # database being locked is what the retries and timeouts # are for: msg = ('{} database timeout for image "{}", writing to ' 'file "{}", {} s') print(msg.format(db.dt_now_str(), db_file, write_file, n_tries * db_timeout)) n_tries += 1 else: # everything else needs to be reported and raised # immediately: msg = '{} for file {}'.format(op_err, db_file) raise sqlite3.OperationalError(msg) except Exception: raise if n_tries > db_attempts: raise sqlite3.OperationalError(op_err) if verbose: msg = 'Database write took: {}' print(msg.format(str(datetime.now() - db_st)))
[docs]def image_file_postproc(filename, outfile=None, img_buf=None, img_dpi=None, img_converter=0, do_trim=False, trim_border=0, logo_file=None, logo_width=None, logo_height=None, logo_padding=0, logo_pos=0, do_thumb=False, img_tags=None, verbose=False): ''' Does the image post-processing for :func:`ImageMetaTag.savefig`. Arguments: filename the name of the image file to process Options: * outfile - If supplied, the processing will be applied to a new file, \ with this name. If not supplied, the post processing will \ overwrite the file given input file. * img_buf - If the image has been saved to an in-memory buffer, then \ supply the image buffer here. This will speed up the \ post-processing. * img_dpi - output image dpi (if available for the image format) \ as a tuple for horizontal/vertical values eg: (72, 72) * img_converter - an integer switch controlling the level of file size \ compression * 0 - no compression * 1 - light compression, from RGBA to RGB * 2 - moderate compression, from RGBA to RGB, then to an \ adaptive 256 colour palette. * 3 - heavy compression, from RGBA to RGB, then to 8-bit \ web standard palette. * do_trim - switch to trim whitespace from the edge of the image * trim_border - if do_trim then this can be used to define an integer \ number of pixels as a border around the trim. * logo_file - a file, or list of image files, to be added as logo(s) to \ the image. When multiple files are added to the same \ logo_pos then they are grouped horizontally from left to \ right before being added. * logo_width - the desired width of a single logo, in pixels. If the \ supplied image file is not the right size, it will be \ resized using a method that applies filters and \ antialiasing that works well for shrinking images with \ text to a much smaller size. The aspect ratio of the logo \ image is always maintained. Either logo_width or \ logo_height need to be specified, and width overrides \ height if both are specified. Defaults to 40 pixels. * logo_height - the desired height of each logo, in pixels, instead of \ logo_width. * logo_padding - a number of pixels to pad around the logo \ (default to zero) * logo_pos - corner, or list of corners, of the logo(s) (following \ pyplot.legend, but for corners): * 0: 'best' in this context will be upper left (default) * 1: 'upper right' (image grows in width to fit) * 2: 'upper left' (image grows in width to fit) * 3: 'lower left' (image grows in height to fit) * 4: 'lower right' (image grows in height to fit) * do_thumb - switch to produce default sized thumbnail, or integer/tuple \ to define the maximum size in pixels * img_tags: a dictionary of tags to be added to the image metadata * verbose: switch for verbose output (reports file sizes before/after \ conversion) ''' # if both logo_width and height are None, apply the defualt to the width: if logo_height is None and logo_width is None: logo_size = {'w': 40} elif logo_height is None: logo_size = {'w': logo_width} elif logo_width is None: logo_size = {'h': logo_height} else: # if both height and width are set, only apply the width: logo_size = {'w': logo_width} # usually, this is used to overwrite a file, but an outfile can be # specified: if not outfile: outfile = filename if verbose: if img_buf: st_fsize = int(sys.getsizeof(img_buf)) else: st_fsize = os.path.getsize(filename) if not (img_tags is None or isinstance(img_tags, dict)): raise ValueError('Image tags must be supplied as a dictionary') if img_converter not in range(4): raise ValueError('Unavailable method for image conversion') # do_thumb should equate to integer type or be a tuple of integers # # this test is taking advantage of the isinstance(do_thumb, tutple) being # true before testing the contents of do_thumb. # Also that as a bool, do_thumb also passes isinstance(do_thumb, int). if not isinstance(do_thumb, int) or (isinstance(do_thumb, tuple) and isinstance(do_thumb[0], int) and isinstance(do_thumb[1], int)): raise ValueError('Invalid thumbnail size') # do we do any image modification at all? modify = (do_trim or do_thumb or img_tags or img_converter > 0 or logo_file is not None) if img_buf: # if the image is in a buffer, then load it now im_obj = Image.open(img_buf) if not modify: # if we're not doing anyhting, then save it: im_obj.save(outfile, dpi=img_dpi, optimize=True) else: if modify: # use the image library to open the file: im_obj = Image.open(filename) if do_trim: # call the _im_trim routine defined above: im_obj = _im_trim(im_obj, border=trim_border) if logo_file is not None: im_obj = _im_logos(im_obj, logo_file, logo_size, logo_padding, logo_pos) if do_thumb: # make a thumbnail image here, if required. It is important to do this # before we change the colour pallette of the main image, so that there # are sufficent colours to do the interpolation. Afterwards, the # thumbnail can hage its colour table reduced as well. # # set a default thumbnail directory name and determine relative paths thumb_dir_name = THUMB_DEFAULT_DIR_NAME thumb_directory = os.path.join(os.path.split(outfile)[0], thumb_dir_name) thumb_full_path = os.path.join(thumb_directory, os.path.split(outfile)[1]) # create thumbnail directory if one does not exist if not os.path.isdir(thumb_directory): os.mkdir(thumb_directory) # set to default thumbnail size if no size specified if do_thumb is True: do_thumb = THUMB_DEFAULT_IMG_SIZE # check input elif not isinstance(do_thumb, tuple): do_thumb = (do_thumb, do_thumb) # create the thumbnail im_thumb = im_obj.copy() im_thumb.thumbnail(do_thumb, Image.ANTIALIAS) # images start out as RGBA, strip out the alpha channel first by # converting to RGB,then you convert to the next format # (that's key to keeping image quality, I think): if img_converter == 1: # this is a good quality image, but not very much smaller: im_obj = im_obj.convert('RGB') if do_thumb: im_thumb = im_thumb.convert('RGB') elif img_converter == 2: # second conversion to 8-bit 'P', palette mode with an adaptive # palette. works well for line plots. im_obj = _im_P256(im_obj) if do_thumb: im_thumb = _im_P256(im_thumb) elif img_converter == 3: # this is VERY strong optimisation and the result can be speckly. im_obj = _im_PWEB(im_obj) if do_thumb: im_thumb = _im_PWEB(im_thumb) if do_thumb: # now save the thumbnail: if img_tags: # add the tags im_thumb = _im_add_png_tags(im_thumb, img_tags) # and save with metadata _im_pngsave_addmeta(im_thumb, thumb_full_path, optimize=True, verbose=verbose) # set a thumbnail directory tag for the main image img_tags.update({'thumbnail directory': thumb_dir_name}) else: # simple save im_thumb.save(thumb_full_path, optimize=True) # now save the main image:n if img_tags: # add the tags im_obj = _im_add_png_tags(im_obj, img_tags) # and save with metadata _im_pngsave_addmeta(im_obj, outfile, img_dpi=img_dpi, optimize=True, verbose=verbose) elif modify: # simple save im_obj.save(outfile, dpi=img_dpi, optimize=True) if verbose: # now report the file size change: en_fsize = os.path.getsize(outfile) msg = 'File: "{}". Size: {}, to {} bytes ({}% original size)' relative_size = (100.0 * en_fsize)/st_fsize print(msg.format(filename, st_fsize, en_fsize, relative_size))
def _im_P256(im_obj): 'Converts an image object to "P" pallette mode with 256 colours (8-bit)' # convert to RGB first to get rid of alpha channel: tmp_im = im_obj.convert('RGB') # then return that, converted to 8bit P: return tmp_im.convert('P', palette=Image.ADAPTIVE, colors=256) def _im_PWEB(im_obj): 'Converts an image to "P" pallette mode with stadnard web colors' tmp_im = im_obj.convert('RGB') return tmp_im.convert('P', palette=Image.WEB) def _im_trim(im_obj, border=0): 'Trims an image object using Python Image Library' if not isinstance(border, int): msg = 'Input border must be an int, but is "{}", type {} instead' raise ValueError(msg.format(border, type(border))) # get the bounding box for the trim: bbox = _im_getbbox_for_trim(im_obj) if border != 0 and bbox is not None: border_bbox = [-border, -border, border, border] # now apply that trim: try: bbox_tr = [x+y for x, y in zip(bbox, border_bbox)] except: raise ValueError('bounding box failure') #pdb.set_trace() # bbox defines the first corner as top+left, then the second corner # as bottom+right (not the bottom left corner, and the width, # height from there) if bbox_tr[0] < 0: bbox_tr[0] = 0 if bbox_tr[1] < 0: bbox_tr[1] = 0 if bbox_tr[2] > im_obj.size[0]: bbox_tr[2] = im_obj.size[0] if bbox_tr[3] > im_obj.size[1]: bbox_tr[3] = im_obj.size[1] # now check to see if that's actually foing to do anything: if bbox_tr == [0, 0, im_obj.size[0], im_obj.size[1]]: bbox = None else: bbox = bbox_tr if bbox: # crop: im_obj = im_obj.crop(bbox) return im_obj def _im_getbbox_for_trim(im_obj): ''' works out the bounding box that defines the actual conent of the image assuming that the top left pixel is the background colour ''' # make a background using the colour in the top/left corner: bg_col = im_obj.getpixel((1, 1)) backg = Image.new(im_obj.mode, im_obj.size, bg_col) # do an image difference: diff = ImageChops.difference(im_obj, backg) # add it together diff = ImageChops.add(diff, diff, 1.0, -100) # and see what the bbox is of that... bbox = diff.getbbox() if bbox is None: # substitute all backgound color for alpha: im_data = np.array(im_obj) im_red, im_grn, im_blu, im_alp = im_data.T bg_areas = (im_red == bg_col[0]) & (im_grn == bg_col[1]) & (im_blu == bg_col[2]) # now set that as zero im_data[..., :][bg_areas.T] = (0, 0, 0, 0) diff = Image.fromarray(im_data) # and get the bounding box from that bbox = diff.getbbox() return bbox def _im_logos(im_obj, logo_files, logo_size, logo_padding, logo_poss): ''' adds logo or logos to the corners of an image object (usually after an im_trim) ''' # work out what logo files go in what corners: if isinstance(logo_files, str): logo_files = [logo_files] if isinstance(logo_poss, int): logo_poss = [logo_poss] logos_by_corner = {} for logo_file, logo_pos in zip(logo_files, logo_poss): # at this stage, each logo_file should be a string # pointing to a file if not isinstance(logo_file, str): msg = 'logo file is not a string - should be a path but is {}' raise ValueError(msg.format(logo_file)) # and logo_pos should be an intger: if not isinstance(logo_pos, int): msg = 'logo position specified not an int, but is {}' raise ValueError(msg.format(logo_pos)) if logo_pos not in logos_by_corner: logos_by_corner[logo_pos] = [logo_file] else: logos_by_corner[logo_pos].append(logo_file) # now add them: for logo_pos, logo_file in logos_by_corner.items(): if len(logo_file) == 1: logo_file = logo_file[0] else: # multiple files get merged before adding: logo_file = _logo_merge(logo_file, logo_size, logo_padding, im_obj.getpixel((0, 0))) im_obj = _im_logo(im_obj, logo_file, logo_size, logo_padding, logo_pos) return im_obj def _im_logo(im_obj, logo_file, logo_size, logo_padding, logo_pos): ''' adds a logo to the required corner of an image object (usually after an im_trim) ''' if logo_file is None: # somehow got here with a None, do nothing: return im_obj if isinstance(logo_file, str): # load in and resize the logo file image: res_logo_obj = resize_logo(Image.open(logo_file), logo_size) else: # assume this is a pre-loaded/resized logo image: res_logo_obj = logo_file # now pull out a sub image from the main image, that's just where the # logo would go, it it were this is the size we want to have blank, to # put the logo, including padding: req_logo_size = [x + 2*logo_padding for x in res_logo_obj.size] if logo_pos in [0, 2]: corner_coords = (0, 0, req_logo_size[0], req_logo_size[1]) elif logo_pos == 1: corner_coords = (im_obj.size[0] - req_logo_size[0], 0, im_obj.size[0], req_logo_size[1]) elif logo_pos == 3: corner_coords = (0, im_obj.size[1] - req_logo_size[1], req_logo_size[0], im_obj.size[1]) elif logo_pos == 4: corner_coords = (im_obj.size[0] - req_logo_size[0], im_obj.size[1] - req_logo_size[1], im_obj.size[0], im_obj.size[1]) else: msg = 'logo_pos={} is invalid. Valid options in range 0 to 4' raise ValueError(msg) corner_obj = im_obj.crop(corner_coords) # now get a bounding box as though we were trimming this image: bbox = _im_getbbox_for_trim(corner_obj) if bbox is None: # the corner object is empty so no need to offset: offset_x = 0 offset_y = 0 else: if logo_pos in [0, 2]: # as this is the top left corner of a plot, the logo should be # offset only in x (so the title is still at the top) offset_x = req_logo_size[0] - bbox[0] offset_y = 0 elif logo_pos == 1: # top right, again just in x offset_x = bbox[2] offset_y = 0 elif logo_pos in [3, 4]: # both of these are on the bottom, so drop the logo downwards: offset_x = 0 offset_y = bbox[3] # now put that together to make an image: # create the blank image: new_size = list(im_obj.size) new_size[0] += offset_x new_size[1] += offset_y new_obj = Image.new(im_obj.mode, new_size, im_obj.getpixel((0, 0))) # put in the main image and logo at the required positions if logo_pos in [0, 2]: # main image is offset by x and y: im_coords = (offset_x, offset_y) # logo pops in the corner, padded: logo_coords = (logo_padding, logo_padding) elif logo_pos == 1: # main image starts immediately in x, and offset_y in y: im_coords = (0, offset_y) # the logo goes in the top right corner, offset/padded: logo_coords = (new_size[0] - req_logo_size[0] + logo_padding, logo_padding) elif logo_pos == 3: # main image at the top, but can be offset in x: im_coords = (offset_x, 0) # logo logo_coords = (logo_padding, new_size[1] - req_logo_size[1] + logo_padding) elif logo_pos == 4: # main image must start at 0,0 im_coords = (0, 0) # logo needs to go in the bottom right, padded: logo_coords = (new_size[0] - req_logo_size[0] + logo_padding, new_size[1] - req_logo_size[1] + logo_padding) # now put that together to make an image: new_obj.paste(im_obj, im_coords) new_obj.paste(res_logo_obj, logo_coords) return new_obj def resize_logo(logo_obj, logo_size): 'rescale am image to the new width/height' do_resize = False # either resize by width or height, keeping the aspect ratio the same # so work out height from width and vice versa: if 'w' in logo_size: logo_width = int(logo_size['w']) if logo_width != logo_obj.size[0]: logo_height = int(logo_obj.size[1] * float(logo_width) / logo_obj.size[0]) do_resize = True elif 'h' in logo_size: logo_height = int(logo_size['h']) if logo_height != logo_obj.size[1]: logo_width = int(logo_obj.size[0] * float(logo_height) / logo_obj.size[1]) do_resize = True # now resize if required: if do_resize: size_tuple = (logo_width, logo_height) res_logo_obj = _img_stong_resize(logo_obj, size=size_tuple) else: res_logo_obj = logo_obj return res_logo_obj def _logo_merge(logo_list, logo_size, padding, bg_col): 'merges a list of image files horizontally, with padding (pixels)' # load up and files that aren't already loaded and get their dims: im_list = [] im_heights = [] im_widths = [] for logo_file in logo_list: if logo_file is None: pass elif isinstance(logo_file, str): im_list.append(resize_logo(Image.open(logo_file), logo_size)) else: # going to assume that this is a pre-loaded image object # as not simple to do a clean for all PIL image formats im_list.append(logo_file) im_widths.append(im_list[-1].size[0]) im_heights.append(im_list[-1].size[1]) # now create the merged image: new_size = [sum(im_widths) + padding, max(im_heights)] merged = Image.new(im_list[0].mode, new_size, bg_col) # and put the images in: current_x = 0 for i_logo, logo_obj in enumerate(im_list): # if the logo has an alpha channel, make a new version with # the new background color: if logo_obj.mode == 'RGBA': # create a new version of logo_obj with the correct background # color: tmp_logo = Image.new(mode='RGB', size=logo_obj.size, color=bg_col) # paste the thumbnail into the full sized image tmp_logo.paste(logo_obj, (0, 0), mask=logo_obj.split()[3]) else: tmp_logo = logo_obj # vertically centre this logo: y_offset = int((new_size[1] - logo_obj.size[1]) / 2) # add the image: merged.paste(tmp_logo, (current_x, y_offset)) # and increment the x: current_x += logo_obj.size[0] + padding return merged def _im_add_png_tags(im_obj, png_tags): 'adds img_tags to an image object for later saving' for key, val in png_tags.items(): im_obj.info[key] = val return im_obj def _im_pngsave_addmeta(im_obj, outfile, img_dpi=None, optimize=True, verbose=False): 'saves an image object to a png file, adding metadata using the info tag.' # undocumented class meta = PngImagePlugin.PngInfo() # copy metadata into new object for key, val in im_obj.info.items(): if key in RESERVED_TAGS: pass elif val is None: if verbose: print('key "%s" is set to None' % key) else: if isinstance(val, str): meta.add_text(key, val, 0) else: msg = ('WARNING: metadata key "{}" skipped as it is type "{}"' ' when it should be a string. Contents: \n "{}"') print(msg.format(key, type(val), val)) # and save im_obj.save(outfile, "PNG", dpi=img_dpi, optimize=optimize, pnginfo=meta) def _img_stong_resize(img_obj, size=None): ''' does image pre-processing before a strong resize, to get rid of halo effects ''' if size is None: size = (40, 40) # shrink_ratio = [x/float(y) for x,y in zip(img_obj.size, size)] # make sure the image has an alpha channel: img_obj = img_obj.convert('RGBA') # premultiply the alpha channel: new_img_obj = _img_premultiplyAlpha(img_obj) # and smooth is the change is size is large: # if max(shrink_ratio) >= 2: # new_img_obj = new_img_obj.filter(ImageFilter.SMOOTH) # now resize: res_img_obj = new_img_obj.resize(size, Image.ANTIALIAS) return res_img_obj def _img_premultiplyAlpha(img_obj): ''' Premultiplies an input image by its alpha channel, which is useful for strong resizes ''' # fake transparent image to blend with transparent = Image.new("RGBA", img_obj.size, (0, 0, 0, 0)) # blend with transparent image using own alpha return Image.composite(img_obj, transparent, img_obj)