# HG changeset patch # User Thierry Florac # Date 1441200659 -7200 # Node ID b3631582e30cb54fa10f47a71f74f8aa6b902c53 # Parent 188b50ad1d39af4c6c8593a90202c46260d17cec Added watermark utility diff -r 188b50ad1d39 -r b3631582e30c src/pyams_file/interfaces/__init__.py --- a/src/pyams_file/interfaces/__init__.py Wed Jul 22 16:56:53 2015 +0200 +++ b/src/pyams_file/interfaces/__init__.py Wed Sep 02 15:30:59 2015 +0200 @@ -238,7 +238,7 @@ def get_thumbnail_name(self, thumbnail_name, with_size=None): """Get matching name for the given thumbnail name or size""" - def get_thumbnail(self, thumbnail_name, format=None): + def get_thumbnail(self, thumbnail_name, format=None, watermark=None): """Get requested thumbnail Display can be specified as: @@ -253,3 +253,10 @@ def clear_thumbnails(self): """Remove all thumbnails from object annotations""" + + +class IWatermarker(Interface): + """Interface of utility used to add image watermark""" + + def add_watermark(self, image, watermark, position='scale', opacity=1, format=None): + """Add watermark to given image""" diff -r 188b50ad1d39 -r b3631582e30c src/pyams_file/thumbnail.py --- a/src/pyams_file/thumbnail.py Wed Jul 22 16:56:53 2015 +0200 +++ b/src/pyams_file/thumbnail.py Wed Sep 02 15:30:59 2015 +0200 @@ -18,7 +18,7 @@ import transaction # import interfaces -from pyams_file.interfaces import IImage, IThumbnail, IThumbnailer, IFileModifiedEvent +from pyams_file.interfaces import IImage, IThumbnail, IThumbnailer, IFileModifiedEvent, IWatermarker from pyams_utils.interfaces.tales import ITALESExtension from zope.annotation.interfaces import IAnnotations from zope.traversing.interfaces import ITraversable @@ -27,6 +27,7 @@ from persistent.dict import PersistentDict from pyams_file.file import FileFactory from pyams_utils.adapter import ContextAdapter, ContextRequestViewAdapter, adapter_config +from pyams_utils.registry import query_utility from pyams_utils.request import check_request from pyramid.events import subscriber from pyramid.threadlocal import get_current_registry @@ -130,7 +131,7 @@ else: return None, None - def get_thumbnail(self, thumbnail_name, format=None): + def get_thumbnail(self, thumbnail_name, format=None, watermark=None): # check for existing thumbnail if thumbnail_name in self.thumbnails: return self.thumbnails[thumbnail_name] @@ -161,6 +162,13 @@ thumbnail_image, format = thumbnail_image else: format = 'jpeg' + # check watermark + if watermark is not None: + watermarker = query_utility(IWatermarker) + if watermarker is not None: + thumbnail_image.seek(0) + thumbnail_image, format = watermarker.add_watermark(thumbnail_image, watermark) + # create final image thumbnail_image = FileFactory(thumbnail_image) registry.notify(ObjectCreatedEvent(thumbnail_image)) self.thumbnails[name] = thumbnail_image diff -r 188b50ad1d39 -r b3631582e30c src/pyams_file/watermark.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/watermark.py Wed Sep 02 15:30:59 2015 +0200 @@ -0,0 +1,93 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import os.path + +from io import StringIO, BytesIO + +# import interfaces +from pyams_file.interfaces import IWatermarker, IImage + +# import packages +from PIL import Image, ImageEnhance +from pyams_utils.registry import utility_config + + +@utility_config(provides=IWatermarker) +class ImageWatermarker(object): + """Image watermarker utility""" + + def _reduce_opacity(self, image, opacity): + """Returns an image with reduced opacity.""" + assert 0 <= opacity <= 1 + if image.mode != 'RGBA': + image = image.convert('RGBA') + else: + image = image.copy() + alpha = image.split()[3] + alpha = ImageEnhance.Brightness(alpha).enhance(opacity) + image.putalpha(alpha) + return image + + def add_watermark(self, image, watermark, position='scale', opacity=1, format=None): + """Adds a watermark to an image and return a new image""" + # init image + if IImage.providedBy(image): + image = image.data + if isinstance(image, bytes): + image = BytesIO(image) + elif isinstance(image, str) and not os.path.exists(image): + image = StringIO(image) + image = Image.open(image) + # check format + if not format: + format = image.format + format = format.upper() + if format not in ('GIF', 'JPEG', 'PNG'): + format = 'JPEG' + # check RGBA mode + if image.mode != 'RGBA': + image = image.convert('RGBA') + # init watermark + if isinstance(watermark, str) and os.path.exists(watermark): + watermark = Image.open(watermark) + else: + if IImage.providedBy(watermark): + watermark = Image.open(StringIO(watermark.data)) + else: + watermark = Image.open(watermark) + if opacity < 1: + watermark = self._reduce_opacity(watermark, opacity) + # create a transparent layer the size of the image and draw the + # watermark in that layer. + layer = Image.new('RGBA', image.size, (0, 0, 0, 0)) + if position == 'tile': + for y in range(0, image.size[1], watermark.size[1]): + for x in range(0, image.size[0], watermark.size[0]): + layer.paste(watermark, (x, y)) + elif position == 'scale': + # scale, but preserve the aspect ratio + ratio = min(float(image.size[0]) / watermark.size[0], float(image.size[1]) / watermark.size[1]) + w = int(watermark.size[0] * ratio) + h = int(watermark.size[1] * ratio) + watermark = watermark.resize((w, h)) + layer.paste(watermark, (int((image.size[0] - w) / 2), int((image.size[1] - h) / 2))) + else: + layer.paste(watermark, position) + # composite the watermark with the layer + new = BytesIO() + Image.composite(layer, image, layer).save(new, format) + return new.getvalue(), format.lower()