'''
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
# image manipulations:
from PIL import Image, ImageChops, PngImagePlugin
import numpy as np
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
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()
if hasattr(Image, "LANCZOS"):
im_thumb.thumbnail(do_thumb, Image.LANCZOS)
else:
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:
if hasattr(Image, "LANCZOS"):
res_img_obj = new_img_obj.resize(size, Image.LANCZOS)
else:
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)