diff --git a/batiskaf/jinja2.py b/batiskaf/jinja2.py index 91ee1ec..eaa3141 100644 --- a/batiskaf/jinja2.py +++ b/batiskaf/jinja2.py @@ -4,6 +4,7 @@ from django.core.urlresolvers import reverse from batiskaf.jinja2_ext.thumbnails import thumbnail from batiskaf.jinja2_ext.cart import cart from batiskaf.jinja2_ext.currency import currency +from batiskaf.jinja2_ext.watermarks import watermark from batiskaf.jinja2_ext.html_filters import * from jinja2 import Environment from store.models import Category, Brand, Currency @@ -22,6 +23,7 @@ def environment(**options): env.filters['thumbnail'] = thumbnail env.filters['cart'] = cart env.filters['currency'] = currency + env.filters['watermark'] = watermark env.filters['bootstrap'] = bootstrap env.filters['bootstrap_inline'] = bootstrap_inline env.filters['bootstrap_horizontal'] = bootstrap_horizontal diff --git a/batiskaf/jinja2_ext/currency.py b/batiskaf/jinja2_ext/currency.py index bffee30..7af7248 100644 --- a/batiskaf/jinja2_ext/currency.py +++ b/batiskaf/jinja2_ext/currency.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- from store.currency import Currency diff --git a/batiskaf/jinja2_ext/watermarks.py b/batiskaf/jinja2_ext/watermarks.py new file mode 100644 index 0000000..06b7e50 --- /dev/null +++ b/batiskaf/jinja2_ext/watermarks.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from watermarker import core + + +def watermark(url, wm): + return core.watermark(url, wm) diff --git a/batiskaf/settings.py b/batiskaf/settings.py index a045cd7..500b391 100644 --- a/batiskaf/settings.py +++ b/batiskaf/settings.py @@ -62,6 +62,7 @@ INSTALLED_APPS = ( 'django.contrib.sitemaps', 'django.contrib.sites', #'django_extensions', + 'watermarker', 'rest_framework', 'easy_thumbnails', 'bootstrapform_jinja', diff --git a/batiskaf/templates/jinja2/includes/category_category_thumb.jinja b/batiskaf/templates/jinja2/includes/category_category_thumb.jinja index ccb54e6..0c27948 100644 --- a/batiskaf/templates/jinja2/includes/category_category_thumb.jinja +++ b/batiskaf/templates/jinja2/includes/category_category_thumb.jinja @@ -2,7 +2,7 @@
{% set im = child.image|thumbnail("420x420") %} - Купить {{ child.title }}

diff --git a/batiskaf/templates/jinja2/includes/category_product_thumb.jinja b/batiskaf/templates/jinja2/includes/category_product_thumb.jinja index 25fe254..5c976f4 100644 --- a/batiskaf/templates/jinja2/includes/category_product_thumb.jinja +++ b/batiskaf/templates/jinja2/includes/category_product_thumb.jinja @@ -7,7 +7,7 @@ {% endif %} {% set im = product.main_image()|thumbnail("420x420") %} - Купить {{ product.title }}
diff --git a/batiskaf/templates/jinja2/index.jinja b/batiskaf/templates/jinja2/index.jinja index 565dac4..74f3eea 100644 --- a/batiskaf/templates/jinja2/index.jinja +++ b/batiskaf/templates/jinja2/index.jinja @@ -36,7 +36,7 @@
{% set im = product.main_image()|thumbnail("420x420") %} - Купить {{ product.title }} diff --git a/batiskaf/templates/jinja2/product.jinja b/batiskaf/templates/jinja2/product.jinja index 3ec8f5e..98c7ed2 100644 --- a/batiskaf/templates/jinja2/product.jinja +++ b/batiskaf/templates/jinja2/product.jinja @@ -36,9 +36,9 @@
{% set im = product.main_image()|thumbnail("398x398") %} - {{ product.title }} + data-zoom-image='/static/{{ product.main_image().url|watermark('big-photo')}}'/>

@@ -149,7 +149,7 @@
{% set im = product.main_image()|thumbnail("420x420") %} - Купить {{ product.title }} diff --git a/batiskaf/templates/jinja2/promo/list.jinja b/batiskaf/templates/jinja2/promo/list.jinja index ce161bf..ffdd469 100644 --- a/batiskaf/templates/jinja2/promo/list.jinja +++ b/batiskaf/templates/jinja2/promo/list.jinja @@ -39,7 +39,7 @@

{# {% set im = object.image|thumbnail("400x400") %}#} - {{ object.title }} diff --git a/static/watermarks/logo-all-small.png b/static/watermarks/logo-all-small.png new file mode 100644 index 0000000..44ab2b7 Binary files /dev/null and b/static/watermarks/logo-all-small.png differ diff --git a/static/watermarks/logo-small-45.png b/static/watermarks/logo-small-45.png new file mode 100644 index 0000000..0cb75a6 Binary files /dev/null and b/static/watermarks/logo-small-45.png differ diff --git a/watermarker/__init__.py b/watermarker/__init__.py new file mode 100644 index 0000000..07f744c --- /dev/null +++ b/watermarker/__init__.py @@ -0,0 +1 @@ +__version__ = '1.3.3' diff --git a/watermarker/admin.py b/watermarker/admin.py new file mode 100644 index 0000000..5d8eaa5 --- /dev/null +++ b/watermarker/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin + +from watermarker import models + + +class WatermarkAdmin(admin.ModelAdmin): + list_display = ('title', 'get_position', 'opacity', 'is_active') + search_fields = ('title',) + fieldsets = ( + (None, {'fields': ('title', 'mark', 'opacity', 'position', ('x', 'y'), 'is_active', 'update_hard'), + 'classes': ('wide',)}), + ) + + +admin.site.register(models.Watermark, WatermarkAdmin) diff --git a/watermarker/core.py b/watermarker/core.py new file mode 100644 index 0000000..0f5ec38 --- /dev/null +++ b/watermarker/core.py @@ -0,0 +1,168 @@ +# coding: utf-8 +import os +import urllib +from logging import error + +from django.db.models.fields.files import ImageFieldFile, ImageField +from django.conf import settings +from PIL import Image, ImageEnhance + +from watermarker import models + + +def reduce_opacity(im, opacity): + """Returns an image with reduced opacity.""" + + assert 0 <= opacity <= 1 + if im.mode != 'RGBA': + im = im.convert('RGBA') + else: + im = im.copy() + alpha = im.split()[3] + alpha = ImageEnhance.Brightness(alpha).enhance(opacity) + im.putalpha(alpha) + return im + + +def make_watermark(im, mark, position, opacity=1, shift=(0, 0)): + """Adds a watermark to an image.""" + + if opacity < 1: + mark = reduce_opacity(mark, opacity) + if im.mode != 'RGBA': + im = im.convert('RGBA') + # create a transparent layer the size of the image and draw the + # watermark in that layer. + layer = Image.new('RGBA', im.size, (0, 0, 0, 0)) + if position == 'tile': + for y in range(0, im.size[1], mark.size[1]): + for x in range(0, im.size[0], mark.size[0]): + layer.paste(mark, (x, y)) + elif position == 'scale': + # scale, but preserve the aspect ratio + ratio = min( + float(im.size[0]) / mark.size[0], float(im.size[1]) / mark.size[1]) + w = int(mark.size[0] * ratio) + h = int(mark.size[1] * ratio) + mark = mark.resize((w, h)) + layer.paste(mark, ((im.size[0] - w) / 2, (im.size[1] - h) / 2)) + elif position == 'br': + x = max(im.size[0] - mark.size[0], 0) - shift[0] + y = max(im.size[1] - mark.size[1], 0) - shift[1] + layer.paste(mark, (x, y)) + elif position == 'tr': + x = max(im.size[0] - mark.size[0], 0) - shift[0] + y = 0 + shift[1] + layer.paste(mark, (x, y)) + elif position == 'bl': + x = 0 + shift[0] + y = max(im.size[1] - mark.size[1], 0) - shift[1] + layer.paste(mark, (x, y)) + elif position == 'tl': + x = 0 + shift[0] + y = 0 + shift[1] + layer.paste(mark, (x, y)) + else: + layer.paste(mark, (0, 0)) + # composite the watermark with the layer + return Image.composite(layer, im, layer) + + +def get_path(url): + """Makes a filesystem path from the specified URL""" + + if url.startswith(settings.MEDIA_URL): + root = settings.MEDIA_ROOT + else: + root = settings.STATIC_ROOT + + url = url.replace(settings.MEDIA_URL, '').replace(settings.STATIC_URL, '') + url = urllib.parse.unquote(url) + return os.path.normpath(os.path.join(root, url)) + + +def make_iff_instance(path): + """ Returns ImageFieldFile for provided image path """ + + return ImageFieldFile(instance=None, field=ImageField(), name=path) + + +def watermark(url, wm): + # return usual url(string) or ImageFieldFile + result_as_iff = False + + if isinstance(wm, models.Watermark): + watermark = wm + else: + try: + watermark = models.Watermark.objects.get(title=wm, is_active=True) + except (models.Watermark.DoesNotExist, models.Watermark.MultipleObjectsReturned): + return url + + if not watermark.is_active: + return url + + # to work not only with strings + if isinstance(url, ImageFieldFile): + result_as_iff = True + if hasattr(url, 'url'): + url = url.url + else: + return url + + basedir = '%s/watermarked' % os.path.dirname(url) + base, ext = os.path.splitext(os.path.basename(url)) + + # watermarked url for template + wm_url = os.path.join(basedir, '%s%s' % (base, ext)) + # path to save watermarked img + wm_path = get_path(wm_url) + + # not to do more than we need + if os.path.exists(wm_path) and not watermark.update_hard: + if result_as_iff: + return make_iff_instance(wm_path) + return wm_url + + # create a folder for watermarks if needed + wm_dir = os.path.dirname(wm_path) + if not os.path.exists(wm_dir): + os.makedirs(wm_dir) + + img_path = get_path(url) + + if not os.path.exists(img_path): + return url + + try: + img = Image.open(img_path) + + mark = Image.open(watermark.mark.path) + x = watermark.x or 0 + y = watermark.y or 0 + shift = (x, y) + position = watermark.position + + opacity = watermark.opacity + + marked_img = make_watermark(img, mark, position, opacity, shift) + marked_img.save(wm_path) + except Exception as e: + error("Cant create watermark for %s. Error type: %s. Msg: %s" % (img_path, type(e), e)) + return url + + if result_as_iff: + return make_iff_instance(wm_path) + + return wm_url + + +def get_url_safe(path): + """ If we save wm with result_as_iff == True, we thumbnail returns us full path to picture (not valid url) + may be there is prettier way to manage this + + """ + + if not path.startswith(settings.MEDIA_URL): + return os.path.join(settings.MEDIA_URL, path.split(settings.MEDIA_URL)[-1]) + return path diff --git a/watermarker/migrations/0001_initial.py b/watermarker/migrations/0001_initial.py new file mode 100644 index 0000000..34a9893 --- /dev/null +++ b/watermarker/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.3 on 2017-05-05 15:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Watermark', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=32, unique=True, verbose_name='Заголовок')), + ('mark', models.ImageField(upload_to='watermarks', verbose_name='Водный знак')), + ('opacity', models.FloatField(default=1, help_text='Значение должно быть от 0 до 1', verbose_name='Непрозрачность')), + ('position', models.CharField(choices=[('tile', 'плиточно'), ('scale', 'масштабировано'), ('br', 'нижний правый угол'), ('tr', 'верхний правый угол'), ('bl', 'нижний левый угол'), ('tl', 'верхний левый угол')], max_length=8, verbose_name='Расположение')), + ('x', models.IntegerField(blank=True, default=0, null=True, verbose_name='Отступ по оси Х')), + ('y', models.IntegerField(blank=True, default=0, null=True, verbose_name='Отступ по оси У')), + ('is_active', models.BooleanField(default=True, verbose_name='Активна')), + ('update_hard', models.BooleanField(default=False, help_text='Используйте, если хотите обновить уже созданные ватермарки. Бьет по производительности', verbose_name='Агрессивно обновлять ватермарки')), + ], + options={ + 'verbose_name': 'Водный знак', + 'verbose_name_plural': 'Водные знаки', + }, + ), + ] diff --git a/watermarker/migrations/__init__.py b/watermarker/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/watermarker/models.py b/watermarker/models.py new file mode 100644 index 0000000..32393be --- /dev/null +++ b/watermarker/models.py @@ -0,0 +1,52 @@ +from django.db import models +from django.core.exceptions import ValidationError + + +class Watermark(models.Model): + OPACITY_ERROR_TEXT = 'Непрозрачность должна быть должна быть между 0 и 1. Это правило!' + + TILE = 'tile' + SCALE = 'scale' + BR = 'br' + TR = 'tr' + BL = 'bl' + TL = 'tl' + + POSITIONS = ( + (TILE, 'плиточно'), + (SCALE, 'масштабировано'), + (BR, 'нижний правый угол'), + (TR, 'верхний правый угол'), + (BL, 'нижний левый угол'), + (TL, 'верхний левый угол'), + ) + + title = models.CharField(max_length=32, verbose_name='Заголовок', unique=True) + mark = models.ImageField(upload_to='watermarks', verbose_name='Водный знак') + opacity = models.FloatField(default=1, verbose_name='Непрозрачность', help_text='Значение должно быть от 0 до 1') + position = models.CharField(max_length=8, verbose_name='Расположение', choices=POSITIONS) + x = models.IntegerField(blank=True, null=True, verbose_name='Отступ по оси Х', default=0) + y = models.IntegerField(blank=True, null=True, verbose_name='Отступ по оси У', default=0) + is_active = models.BooleanField(default=True, verbose_name='Активна') + update_hard = models.BooleanField(default=False, verbose_name='Агрессивно обновлять ватермарки', + help_text='Используйте, если хотите обновить уже созданные ватермарки. ' + 'Бьет по производительности') + + def get_position(self): + return self.get_position_display() + + def clean(self): + if not 0 <= self.opacity <= 1: + raise ValidationError(self.OPACITY_ERROR_TEXT) + + def __unicode__(self): + return self.title + + def __str__(self): + return self.title + + get_position.short_description = 'Расположение' + + class Meta: + verbose_name = 'Водный знак' + verbose_name_plural = 'Водные знаки'