parent
e169d609da
commit
1be1ffdf24
17 changed files with 286 additions and 8 deletions
@ -0,0 +1,6 @@ |
||||
# -*- coding: utf-8 -*- |
||||
from watermarker import core |
||||
|
||||
|
||||
def watermark(url, wm): |
||||
return core.watermark(url, wm) |
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 57 KiB |
@ -0,0 +1 @@ |
||||
__version__ = '1.3.3' |
||||
@ -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) |
||||
@ -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 |
||||
@ -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': 'Водные знаки', |
||||
}, |
||||
), |
||||
] |
||||
@ -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 = 'Водные знаки' |
||||
Loading…
Reference in new issue