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