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 = object.image|thumbnail("400x400") %}#}
-
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 = 'Водные знаки'