일단 커밋. 오랫동안 커밋을 안해서 꼬였다.

리팩토리 중.
This commit is contained in:
2025-11-15 15:59:49 +09:00
parent 5a47b792d6
commit d79c10b975
12909 changed files with 2070539 additions and 285 deletions

View File

@@ -0,0 +1,3 @@
VERSION = (1, 5, 33)
default_app_config = 'image.apps.ImageConfig'

View File

@@ -0,0 +1,20 @@
# coding=utf-8
from django.conf import settings
import sys
__author__ = 'franki'
from django.apps import AppConfig
class ImageConfig(AppConfig):
name = 'image'
label = 'image'
verbose_name = "Image"
# def ready(self):
# if settings.DEBUG and not 'django.template.context_processors.request' in settings.TEMPLATE_CONTEXT_PROCESSORS:
# print >> sys.stderr, \
# "image: Add 'django.template.context_processors.request' to TEMPLATE_CONTEXT_PROCESSORS in order to\n" \
# "give access to sessions from templates. Otherwise set autogen=true in all uses. This message only\n" \
# "appears with DEBUG enabled."

View File

@@ -0,0 +1,114 @@
# -*- coding: UTF-8 -*-
import six
from django.db import models
from django.db.models.signals import post_init
from image.forms import ImageCenterFormField
from image.video_field import VideoField
class ImageCenter(object):
def __init__(self, image_field, x=None, y=None, xy=None):
self.image_field = image_field
if (not x is None and y is None) or (x is None and not y is None):
raise ValueError(
u"If x or y are provided, both have to be provided: x=" + six.text_type(x) + u", y=" + six.text_type(y))
if not x is None and not y is None:
if x < 0 or x > 1 or y < 0 or y > 1:
raise ValueError(
u"Valid values for x and y go from 0 to 1: x=" + six.text_type(x) + u", y=" + six.text_type(y))
self.x = float(x)
self.y = float(y)
else:
if xy is None:
self.x = .5
self.y = .5
else:
try:
x, y = xy.split(",")
except ValueError:
x = .5
y = .5
self.x = float(x)
self.y = float(y)
def __str__(self):
return str(self.x) + "," + str(self.y)
__unicode__ = __str__
class ImageCenterField(models.Field):
attr_class = ImageCenter
description = "A field that stores the center of attention for an image."
# __metaclass__ = models.SubfieldBase
def __init__(self, image_field=None, *args, **kwargs):
if image_field is not None:
if not isinstance(image_field, models.ImageField) and not isinstance(image_field, VideoField):
raise ValueError("image_field value must be an ImageField or VideoField instance")
kwargs["default"] = ".5,.5"
self.image_field = image_field
super(ImageCenterField, self).__init__(*args, **kwargs)
def set_instance(self, instance):
self.instance = instance
def formfield(self, **kwargs):
defaults = {'form_class': ImageCenterFormField}
defaults.update(kwargs)
return super(ImageCenterField, self).formfield(**defaults)
def db_type(self, connection):
return "char(100)"
# Esta función es llamada al leer un valor de la base de datos
def to_python(self, value):
if isinstance(value, ImageCenter):
return value
return ImageCenter(self.image_field, xy=value)
# Esta función es llamada al escribir un valor en la base de datos
def get_db_prep_value(self, value, connection=None, prepared=False):
try:
return str(value.x) + "," + str(value.y)
except AttributeError:
return str(value)
def from_db_value(self, value, expression, connection, context=None):
return ImageCenter(self.image_field, xy=value)
def value_to_string(self, obj):
value = getattr(obj, self.attname)
return self.get_db_prep_value(value)
def query_string(self):
return "center=" + str(self.x) + "," + str(self.y)
def post_init_capture(sender, instance, *args, **kwargs):
fields = instance.__class__._meta.get_fields()
for field in fields:
if isinstance(field, ImageCenterField) and field.image_field:
image_field = instance.__class__._meta.get_field(field.image_field.name)
image_instance = instance.__getattribute__(image_field.name)
image_center_instance = instance.__getattribute__(field.name)
image_instance.__image_center_instance__ = image_center_instance
if isinstance(image_center_instance, six.string_types):
image_center_instance = ImageCenter(image_field, xy=image_center_instance)
setattr(instance, field.name, image_center_instance)
image_center_instance.image_path = six.text_type(image_instance)
post_init.connect(post_init_capture)
try:
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], [r"^image\.fields\.ImageCenterField$"])
add_introspection_rules([], [r"^image\.video_field\.VideoField$"])
except ImportError:
pass

View File

@@ -0,0 +1,104 @@
# -*- coding: UTF-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import six
from django import forms
from django.forms.utils import flatatt
from django.utils.encoding import force_text
from django.utils.safestring import mark_safe
try:
from django.core.urlresolvers import reverse
except ImportError:
from django.urls import reverse
import threading
from image.video_field import VideoField
COUNTER = 0
class ImageCenterFormWidget(forms.Widget):
#def __init__(self, attrs=None):
# super(ImageCenterFormWidget, self).__init__(attrs)
def _format_value(self, value):
return six.text_type(value)
def render(self, name, value, attrs=None, **kwargs):
global COUNTER
if value is None:
value = ''
try:
final_attrs = self.build_attrs(attrs, name=name)
except TypeError:
final_attrs = self.build_attrs(attrs, {"name": name})
if value != '':
# Only add the 'value' attribute if a value is non-empty.
final_attrs['value'] = force_text(self._format_value(value))
resp = ''
if getattr(value, 'image_path', None):
try:
extra_parms = ""
if isinstance(value.image_field, VideoField):
extra_parms += "&video=true"
extra_parms += "&is_admin=true"
resp = '<div style="display:inline-block; position:relative; border:1px solid black;">'
resp += '<img id="image_center-' + str(COUNTER) + '" src="' + reverse(
'image.views.image', args=('format=png&width=150&height=150&mode=scale' + extra_parms, value.image_path)) + '" onclick=""/>'
resp += '<img id="image_center_crosshair-' + str(COUNTER) + '" src="' + reverse('image.views.crosshair') + '" style="position:absolute; left:0; top:0;" />'
resp += '</div>'
resp += '<script>'
resp += '(function($) {'
resp += ' $(window).on("load", function(){'
resp += ' var crosshair = document.getElementById("image_center_crosshair-' + str(COUNTER) + '");'
resp += ' var image = document.getElementById("image_center-' + str(COUNTER) + '");'
resp += ' var iw = $(image).width();'
resp += ' var ih = $(image).height();'
resp += ' $(crosshair).css( { left : (iw*' + str(value.x) + ' - 7)+"px", top : (ih*' + str(value.y) + ' - 7)+"px" } );'
resp += ' $(image).parent().parent().find("input").hide();'
resp += ' $(image).parent().click(function(e){'
resp += ' var nx = e.pageX - $(image).offset().left;'
resp += ' var ny = e.pageY - $(image).offset().top;'
resp += ' crosshair.style.left=(nx - 7)+"px";'
resp += ' crosshair.style.top=(ny - 7)+"px";'
resp += ' $(image).parent().parent().find("input").val( (nx/iw)+","+(ny/ih) );'
resp += ' });'
resp += '});'
resp += '})(django.jQuery);'
resp += '</script>'
resp += u'<input%s />' % flatatt(final_attrs)
lock = threading.Lock()
with lock:
COUNTER += 1
if COUNTER > 4000000000:
COUNTER = 0
except AttributeError:
resp = 'Only available once the image has been saved'
return mark_safe(resp)
class ImageCenterFormField(forms.Field):
widget = ImageCenterFormWidget
def __init__(self, *args, **kwargs):
kwargs['required'] = False
super(ImageCenterFormField, self).__init__(*args, **kwargs)
def clean(self, value):
value = self.to_python(value)
return value

View File

@@ -0,0 +1,25 @@
#-*- coding: UTF-8 -*-
try:
from django.core.urlresolvers import reverse
except ImportError:
from django.urls import reverse
import six
from image.utils import image_create_token
from image.views import image as image_view
def get_image_url(image, parameters):
if 'autogen=true' in parameters:
image_view(None, str(image), parameters, True)
return reverse(
'image.views.image',
args=(
image_create_token(parameters),
six.text_type(image),
)
)

View File

@@ -0,0 +1,93 @@
from django.core.files.storage import FileSystemStorage
import os
from django.db.models.signals import post_save, post_delete, pre_save
from django.db.models.fields.files import FileField
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.conf import settings as django_settings
from image.storage import IMAGE_CACHE_STORAGE
def safe_delete(path):
if isinstance(IMAGE_CACHE_STORAGE, FileSystemStorage):
full_path = os.path.join(IMAGE_CACHE_STORAGE.location, path)
if os.path.isdir(full_path):
os.rmdir(full_path)
return
IMAGE_CACHE_STORAGE.delete(path)
def remove_directory(dir_path):
try:
# Since no all storages support exists for directories, we check for OSError
contents = IMAGE_CACHE_STORAGE.listdir(dir_path)
except OSError:
pass
else:
for directory in contents[0]:
safe_delete(os.path.join(dir_path, directory))
for filename in contents[1]:
safe_delete(os.path.join(dir_path, filename))
if IMAGE_CACHE_STORAGE.exists(dir_path):
# In some storages like amazon S3 there are no directories
safe_delete(dir_path)
def remove_cache(image_path):
if image_path:
remove_directory(image_path)
def prepare_image_cache_cleanup(sender, instance=None, **kwargs):
if instance is None:
return
instance.old_image_fields = {}
old_instance = None
for field in instance._meta.fields:
if isinstance(field, FileField):
if not old_instance:
try:
old_instance = sender.objects.get(pk=instance.pk)
except (ObjectDoesNotExist, MultipleObjectsReturned):
return
instance.old_image_fields[field.attname] = field.value_to_string(old_instance)
def clear_prepared_image_cache_cleanup(sender, instance=None, created=False, **kwargs):
if created:
return
if instance is None:
return
for field in instance._meta.fields:
if isinstance(field, FileField):
if instance.old_image_fields[field.attname] != field.value_to_string(instance):
remove_cache(instance.old_image_fields[field.attname])
def clear_image_cache(sender, instance, **kwargs):
for field in instance._meta.fields:
if isinstance(field, FileField):
remove_cache(field.value_to_string(instance))
pre_save.connect(prepare_image_cache_cleanup)
post_save.connect(clear_prepared_image_cache_cleanup)
post_delete.connect(clear_image_cache)
#reversion compatibility
if 'reversion' in django_settings.INSTALLED_APPS:
try:
from reversion.models import pre_revision_commit, post_revision_commit
pre_revision_commit.connect(prepare_image_cache_cleanup)
post_revision_commit.connect(clear_prepared_image_cache_cleanup)
except ImportError:
pass
# http://bakery-app01-static.s3.amazonaws.com/image/actuality/file0001116000079.jpg/image_token_73ff82d8ce1577a8a22f5a7d29a772a3ffc6e76c
remove_directory('actuality/file0001116000079.jpg')

View File

@@ -0,0 +1,25 @@
# coding=UTF-8
from django.conf import global_settings
__author__ = 'franki'
from django.conf import settings
def get(key, default):
return getattr(settings, key, default)
IMAGE_DEFAULT_FORMAT = get('IMAGE_DEFAULT_FORMAT', 'JPEG')
IMAGE_DEFAULT_QUALITY = get('IMAGE_DEFAULT_QUALITY', 85)
IMAGE_CACHE_STORAGE = get('IMAGE_CACHE_STORAGE', 'image.storage.ImageCacheStorage')
IMAGE_CACHE_ROOT = get('IMAGE_CACHE_ROOT', None)
# If IMAGE_CACHE_URL differs from the url of the image view then you must always use autogen or have proper rewrite rules on your server
IMAGE_CACHE_URL = get('IMAGE_CACHE_URL', '/image/')
IMAGE_CACHE_HTTP_EXPIRATION = get('IMAGE_CACHE_HTTP_EXPIRATION', 3600 * 24 * 30)
FILE_UPLOAD_TEMP_DIR = get('FILE_UPLOAD_TEMP_DIR', global_settings.FILE_UPLOAD_TEMP_DIR)
STATICFILES_STORAGE = get('STATICFILES_STORAGE', global_settings.STATICFILES_STORAGE)

View File

@@ -0,0 +1,98 @@
# coding=UTF-8
import hashlib
import os
import sys
from os.path import basename, dirname
from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import ContentFile
from django.core.files.storage import get_storage_class, FileSystemStorage
try:
from storages.backends.s3boto import S3BotoStorage
BOTO_IS_AVAILABLE = True
except (ImportError, ImproperlyConfigured):
BOTO_IS_AVAILABLE = False
from image import settings
from image.settings import IMAGE_CACHE_STORAGE as settings_IMAGE_CACHE_STORAGE, STATICFILES_STORAGE
__author__ = 'franki'
STORAGE = None
if BOTO_IS_AVAILABLE:
class LocallyMirroredS3BotoStorage(S3BotoStorage):
def __init__(self, *args, **kwargs):
super(LocallyMirroredS3BotoStorage, self).__init__(*args, **kwargs)
self.mirror = FileSystemStorage(location=settings.S3_MIRROR_ROOT)
def delete(self, name):
super(LocallyMirroredS3BotoStorage, self).delete(name)
try:
self.mirror.delete(name)
except OSError:
full_path = self.mirror.path(name)
if os.path.exists(full_path):
os.rmdir(full_path)
def exists(self, name):
exists_local = self.mirror.exists(name)
if exists_local:
return True
else:
exists_remote = super(LocallyMirroredS3BotoStorage, self).exists(name)
if exists_remote:
self.mirror._save(name, ContentFile(""))
return True
return False
def _save(self, name, content):
cleaned_name = super(LocallyMirroredS3BotoStorage, self)._save(name, content)
self.mirror._save(name, ContentFile(""))
return cleaned_name
else:
class LocallyMirroredS3BotoStorage(object):
def __init__(self, *args, **kwargs):
raise ImportError("In order to use LocallyMirroredS3BotoStorage you need to install django-storages")
class ImageCacheStorage(FileSystemStorage):
autogen_required = False
def __init__(self, location=None, base_url=None, *args, **kwargs):
if location is None:
location = settings.IMAGE_CACHE_ROOT
if base_url is None:
base_url = settings.IMAGE_CACHE_URL
if not location:
raise ImproperlyConfigured("IMAGE_CACHE_ROOT not defined.")
super(ImageCacheStorage, self).__init__(location, base_url, *args, **kwargs)
def path(self, name):
if not self.location:
raise ImproperlyConfigured("IMAGE_CACHE_ROOT not defined.")
return super(ImageCacheStorage, self).path(name)
def save(self, name, content, max_length=None):
super(ImageCacheStorage, self).save(name, ContentFile(content), max_length=max_length)
def get_storage():
global STORAGE
if STORAGE:
return STORAGE
if settings_IMAGE_CACHE_STORAGE:
storage_class = get_storage_class(settings_IMAGE_CACHE_STORAGE)
else:
storage_class = get_storage_class()
STORAGE = storage_class()
return STORAGE
IMAGE_CACHE_STORAGE = get_storage()
MEDIA_STORAGE = get_storage_class()()
STATIC_STORAGE = get_storage_class(STATICFILES_STORAGE)()

View File

@@ -0,0 +1,69 @@
# -*- coding: UTF-8 -*-
import six
from django import template
from django.db.models.fields.files import ImageFieldFile
from django.http.request import HttpRequest
from django.template.defaulttags import register
from image import views as image_views
from image.storage import IMAGE_CACHE_STORAGE
from image.utils import image_url
from image.video_field import VideoFieldFile
class ImageNode(template.Node):
def __init__(self, image_field, parameters):
self.image_field = image_field
self.parameters = parameters # It is not a variable because we compiled filters.
def render(self, context):
try:
request = context['request']
except KeyError:
request = HttpRequest()
image_field = self.image_field.resolve(context)
try:
parameters = self.parameters.resolve(context)
except template.VariableDoesNotExist:
parameters = self.parameters
if isinstance(image_field, VideoFieldFile):
parameters += "&video=true"
if isinstance(image_field, ImageFieldFile) or isinstance(image_field, VideoFieldFile):
try:
parameters = parameters + "&center=" + image_field.__image_center_instance__.__str__()
except AttributeError:
pass
if "autogen=true" in parameters or getattr(IMAGE_CACHE_STORAGE, "autogen_required", False):
# We want the image to be generated immediately
image_views.image(request, six.text_type(image_field), parameters, True)
# image_path = os.path.join(image_tokenize(session, parameters), six.text_type(image_field))
# return IMAGE_CACHE_STORAGE.url(image_path)
return image_url(request, parameters, image_field)
# return reverse(
# 'image.views.image',
# args=(
# str(image_field),
# image_tokenize(session, parameters)
# )
# )
@register.tag(name="image")
def image(parser, token):
try:
# split_contents() knows not to split quoted strings.
tag_name, image_field, parameters = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError("%r tag requires 2 arguments " % token.contents.split()[0])
image_field = parser.compile_filter(image_field)
parameters = parser.compile_filter(parameters)
return ImageNode(image_field, parameters)

View File

@@ -0,0 +1,9 @@
# -*- coding: UTF-8 -*-
from django.conf.urls import url
from image import views
urlpatterns = [
url(r'^crosshair$', views.crosshair, name="image.views.crosshair"),
url(r'^(?P<token>[:\-\w_=&]+)/(?P<path>.+)$', views.image, name="image.views.image"),
]

View File

@@ -0,0 +1,613 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import hashlib
import os
from PIL import Image as pil
from functools import lru_cache
import six
from six import StringIO, BytesIO
from image import settings
from image.settings import IMAGE_DEFAULT_QUALITY, IMAGE_DEFAULT_FORMAT
from image.storage import MEDIA_STORAGE, STATIC_STORAGE, IMAGE_CACHE_STORAGE
INTERNAL_CACHE_ROOT = "%s/_internal/" % settings.IMAGE_CACHE_ROOT
ALPHA_FORMATS = ["PNG"]
def power_to_rgb(value):
if value <= 0.0031308:
value *= 12.92
else:
value = 1.055 * pow(value, 0.416666666667) - 0.055
return round(value * 255.0)
def rgb_to_power(value):
value = float(value) / 255.0
if value <= 0.04045:
value /= 12.92
else:
value = pow((value + 0.055) / 1.055, 2.4)
return value
def add_rgba_to_pixel(pixel, rgba, x_ammount, x_displacement):
a = rgba[3]
pa = pixel[3]
if a == 1.0 and pa == 1.0:
total_ammount = x_ammount + x_displacement
rgba_ammount = x_ammount / total_ammount
pixel_ammount = x_displacement / total_ammount
return (
pixel[0] * pixel_ammount + rgba[0] * rgba_ammount,
pixel[1] * pixel_ammount + rgba[1] * rgba_ammount,
pixel[2] * pixel_ammount + rgba[2] * rgba_ammount,
pa * pixel_ammount + a * rgba_ammount,
)
else:
total_ammount = x_ammount + x_displacement
rgba_ammount = x_ammount / total_ammount
# rgba_ammount_alpha = rgba_ammount * a
pixel_ammount = x_displacement / total_ammount
# pixel_ammount_alpha = pixel_ammount * pa
return (
pixel[0] * pixel_ammount + rgba[0] * rgba_ammount,
pixel[1] * pixel_ammount + rgba[1] * rgba_ammount,
pixel[2] * pixel_ammount + rgba[2] * rgba_ammount,
pa * pixel_ammount + a * rgba_ammount,
)
def resizeScale(img, width, height, force):
src_width, src_height = img.size
src_ratio = float(src_width) / float(src_height)
if force:
max_width = width
max_height = height
else:
max_width = min(width, src_width)
max_height = min(height, src_height)
dst_width = max_width
dst_height = dst_width / src_ratio
if dst_height > max_height:
dst_height = max_height
dst_width = dst_height * src_ratio
# img_width, img_height = img.size
img = img.resize((int(dst_width), int(dst_height)), pil.ANTIALIAS)
return img
def resizeCrop(img, width, height, center, force):
"""
# Esto no hace nada perceptible
RATIO = 2
while img.size[0] / RATIO > width or img.size[1] / RATIO > height:
img = img.resize((int(img.size[0]/RATIO), int(img.size[1]/RATIO)), pil.ANTIALIAS)
"""
max_width = width
max_height = height
if not force:
img.thumbnail((max_width, max_height), pil.ANTIALIAS)
else:
src_width, src_height = img.size
src_ratio = float(src_width) / float(src_height)
dst_width, dst_height = max_width, max_height
dst_ratio = float(dst_width) / float(dst_height)
if dst_ratio < src_ratio:
crop_height = src_height
crop_width = crop_height * dst_ratio
x_offset = float(src_width - crop_width) / 2
y_offset = 0
else:
crop_width = src_width
crop_height = crop_width / dst_ratio
x_offset = 0
y_offset = float(src_height - crop_height) / 2
center_x, center_y = center.split(',')
center_x = float(center_x)
center_y = float(center_y)
x_offset = min(
max(0, center_x * src_width - crop_width / 2),
src_width - crop_width
)
y_offset = min(
max(0, center_y * src_height - crop_height / 2),
src_height - crop_height
)
img = img.crop(
(int(x_offset), int(y_offset), int(x_offset) + int(crop_width), int(y_offset) + int(crop_height)))
img = img.resize((int(dst_width), int(dst_height)), pil.ANTIALIAS)
return img
def do_tint(img, tint):
if not tint or tint == 'None':
return
if img.mode != "RGBA":
img = img.convert("RGBA")
try:
tint_red = float(int("0x%s" % tint[0:2], 16)) / 255.0
except ValueError:
tint_red = 1.0
try:
tint_green = float(int("0x%s" % tint[2:4], 16)) / 255.0
except ValueError:
tint_green = 1.0
try:
tint_blue = float(int("0x%s" % tint[4:6], 16)) / 255.0
except ValueError:
tint_blue = 1.0
try:
tint_alpha = float(int("0x%s" % tint[6:8], 16)) / 255.0
except ValueError:
tint_alpha = 1.0
try:
intensity = float(int("0x%s" % tint[8:10], 16))
except ValueError:
intensity = 255.0
if intensity > 0.0 and (tint_red != 1.0 or tint_green != 1.0 or tint_blue != 1.0 or tint_alpha != 1.0):
# Only tint if the color provided is not ffffffff, because that equals no tint
pixels = img.load()
if intensity == 255.0:
for y in range(img.size[1]):
for x in range(img.size[0]):
data = pixels[x, y]
pixels[x, y] = (
int(float(data[0]) * tint_red),
int(float(data[1]) * tint_green),
int(float(data[2]) * tint_blue),
int(float(data[3]) * tint_alpha),
)
else:
intensity = intensity / 255.0
intensity_inv = 1 - intensity
tint_red *= intensity
tint_green *= intensity
tint_blue *= intensity
tint_alpha *= intensity
for y in range(img.size[1]):
for x in range(img.size[0]):
data = pixels[x, y]
pixels[x, y] = (
int(float(data[0]) * intensity_inv + float(data[0]) * tint_red),
int(float(data[1]) * intensity_inv + float(data[1]) * tint_green),
int(float(data[2]) * intensity_inv + float(data[2]) * tint_blue),
int(float(data[3]) * intensity_inv + float(data[3]) * tint_alpha),
)
def do_grayscale(img):
return img.convert('LA').convert('RGBA')
def do_paste(img, overlay, position):
if overlay.mode != 'RGBA':
overlay = overlay.convert('RGBA')
# img.paste(overlay, position, overlay)
# return img
overlay_full = pil.new('RGBA', img.size, color=(255, 255, 255, 0))
overlay_full.paste(overlay, position)
# img.paste(overlay_full, (0,0), overlay_full)
# return img
return pil.alpha_composite(img, overlay_full)
# overlay_pixels = overlay.load()
# img_pixels = img.load()
# overlay_width, overlay_height = overlay.size
# x_offset, y_offset = position
#
# for y in range(min(overlay_height, img.size[1] - y_offset)):
# for x in range(min(overlay_width, img.size[0] - x_offset)):
# img_pixel = img_pixels[x + x_offset, y + y_offset]
# overlay_pixel = overlay_pixels[x, y]
# ia = img_pixel[3]
# oa = overlay_pixel[3]
# if oa == 0:
# # overlay is transparent, nothing to do
# continue
# elif oa == 255:
# # overlay is opaque, ignore img pixel
# new_pixel = overlay_pixel
# elif ia == 0:
# # image pixel is 100% transparent, only overlay matters
# new_pixel = overlay_pixel
# elif ia == 255:
# # simpler math
# oa = float(oa) / 255.0
# oa1 = 1.0 - oa
# new_pixel = (
# int(power_to_rgb( rgb_to_power(img_pixel[0]) * oa1 + rgb_to_power(overlay_pixel[0]) * oa )),
# int(power_to_rgb( rgb_to_power(img_pixel[1]) * oa1 + rgb_to_power(overlay_pixel[1]) * oa )),
# int(power_to_rgb( rgb_to_power(img_pixel[2]) * oa1 + rgb_to_power(overlay_pixel[2]) * oa )),
# 255,
# )
# else:
# # complex math
# oa = float(oa) / 255.0
# ia = float(ia) / 255.0
# oa1 = 1 - oa
# #total_alpha_percent = oa + ia
# overlay_percent = oa
# image_percent = ia * oa1
#
# new_pixel = (
# int(power_to_rgb( rgb_to_power(img_pixel[0]) * image_percent + rgb_to_power(overlay_pixel[0]) * overlay_percent )),
# int(power_to_rgb( rgb_to_power(img_pixel[1]) * image_percent + rgb_to_power(overlay_pixel[1]) * overlay_percent )),
# int(power_to_rgb( rgb_to_power(img_pixel[2]) * image_percent + rgb_to_power(overlay_pixel[2]) * overlay_percent )),
# int((oa + ia * oa1) * 255.0),
# )
#
# img_pixels[x + x_offset, y + y_offset] = new_pixel
def do_overlay(img, overlay_path, overlay_source=None, overlay_tint=None, overlay_size=None, overlay_position=None):
if not overlay_path:
return img
if overlay_source == 'media':
overlay = pil.open(MEDIA_STORAGE.open(overlay_path))
else:
overlay = pil.open(STATIC_STORAGE.open(overlay_path))
# We want the overlay to fit in the image
iw, ih = img.size
ow, oh = overlay.size
overlay_ratio = float(ow) / float(oh)
if overlay_size:
tw, th = overlay_size.split(',')
ow = int(round(float(tw.strip()) * iw))
oh = int(round(float(th.strip()) * ih))
if ow < 0:
ow = oh * overlay_ratio
elif oh < 0:
oh = ow / overlay_ratio
overlay = resizeScale(overlay, ow, oh, overlay_source + "/" + overlay_path)
ow, oh = overlay.size
else:
have_to_scale = False
if ow > iw:
ow = iw
oh = int(float(iw) / overlay_ratio)
have_to_scale = True
if oh > ih:
ow = int(float(ih) * overlay_ratio)
oh = ih
have_to_scale = True
if have_to_scale:
overlay = resizeScale(overlay, ow, oh, overlay_source + "/" + overlay_path)
ow, oh = overlay.size
if overlay_tint:
do_tint(overlay, overlay_tint)
if not overlay_position:
target_x = int((iw - ow) / 2)
target_y = int((ih - oh) / 2)
else:
tx, ty = overlay_position.split(',')
tx = tx.strip()
ty = ty.strip()
if tx == "":
# Center horizontally.
target_x = int((iw - ow) / 2)
else:
if "!" in tx:
# X origin on the right side.
x_percent = 1.0 - float(tx.replace("!", "").strip())
target_x = int(round(x_percent * iw) - ow)
else:
# X origin on the left side.
x_percent = float(tx)
target_x = int(round(x_percent * iw))
if ty == "":
# Center vertically.
target_y = int((ih - oh) / 2)
else:
if "!" in ty:
# Y origin on the bottom side.
y_percent = 1.0 - float(ty.replace("!", "").strip())
target_y = int(round(y_percent * ih) - oh)
else:
# Y origin on the top side.
y_percent = float(ty)
target_y = int(round(y_percent * ih))
"""
TODO: paste seems to be buggy, because pasting over opaque background returns a non opaque image
(the parts that are not 100% opaque or 100% transparent become partially transparent.
the putalpha workareound doesn't seem to look nice enough
"""
img = do_paste(img, overlay, (target_x, target_y))
return img
def do_overlays(img, overlays, overlay_tints, overlay_sources, overlay_sizes, overlay_positions):
overlay_index = 0
for overlay in overlays:
try:
overlay_tint = overlay_tints[overlay_index]
except (IndexError, TypeError):
overlay_tint = None
if overlay_tint == "None":
overlay_tint = None
try:
overlay_source = overlay_sources[overlay_index]
except (IndexError, TypeError):
overlay_source = 'static'
try:
overlay_size = overlay_sizes[overlay_index]
except (IndexError, TypeError):
overlay_size = None
if overlay_size == "None":
overlay_size = None
try:
overlay_position = overlay_positions[overlay_index]
except (IndexError, TypeError):
overlay_position = None
if overlay_position == "None":
overlay_position = None
img = do_overlay(img, overlay, overlay_source, overlay_tint, overlay_size, overlay_position)
overlay_index += 1
return img
def do_mask(img, mask_path, mask_source, mask_mode=None):
if not mask_path:
return img
if mask_source == 'media':
mask = pil.open(MEDIA_STORAGE.open(mask_path)).convert("RGBA")
else:
mask = pil.open(STATIC_STORAGE.open(mask_path)).convert("RGBA")
# We want the mask to have the same size than the image
if mask_mode == 'distort':
iw, ih = img.size
mw, mh = mask.size
if mw != iw or mh != ih:
mask = mask.resize((iw, ih), pil.ANTIALIAS)
else:
# We want the overlay to fit in the image
iw, ih = img.size
ow, oh = mask.size
overlay_ratio = float(ow) / float(oh)
have_to_scale = False
if ow > iw:
ow = iw
oh = int(float(iw) / overlay_ratio)
if oh > ih:
ow = int(float(ih) * overlay_ratio)
oh = ih
if ow != iw or oh != ih:
have_to_scale = True
if have_to_scale:
nmask = mask.resize((ow, oh), pil.ANTIALIAS)
mask = pil.new('RGBA', (iw, ih))
# mask.paste(nmask, (int((iw - ow) / 2), int((ih - oh) / 2)), nmask)
mask = do_paste(mask, nmask, (int((iw - ow) / 2), int((ih - oh) / 2)))
ow, oh = mask.size
r, g, b, a = mask.split()
img.putalpha(a)
def do_fill(img, fill, width, height):
if not fill:
return img
overlay = img
fill_color = (
int("0x%s" % fill[0:2], 16),
int("0x%s" % fill[2:4], 16),
int("0x%s" % fill[4:6], 16),
int("0x%s" % fill[6:8], 16),
)
img = pil.new("RGBA", (width, height), fill_color)
iw, ih = img.size
ow, oh = overlay.size
# img.paste(overlay, (int((iw - ow) / 2), int((ih - oh) / 2)), overlay)
img = do_paste(img, overlay, (int((iw - ow) / 2), int((ih - oh) / 2)))
return img
def do_padding(img, padding):
if not padding:
return img
try:
padding = float(padding) * 2.0
if padding > .9:
padding = .9
if padding <= 0.0:
return img
except ValueError:
return
iw, ih = img.size
img.thumbnail(
(
int(round(float(img.size[0]) * (1.0 - padding))),
int(round(float(img.size[1]) * (1.0 - padding)))
),
pil.ANTIALIAS
)
img = do_fill(img, "ffffff00", iw, ih)
return img
def do_background(img, background):
if not background:
return img
overlay = img
fill_color = (
int("0x%s" % background[0:2], 16),
int("0x%s" % background[2:4], 16),
int("0x%s" % background[4:6], 16),
int("0x%s" % background[6:8], 16),
)
img = pil.new("RGBA", overlay.size, fill_color)
iw, ih = img.size
ow, oh = overlay.size
# img.paste(overlay, (int((iw - ow) / 2), int((ih - oh) / 2)), overlay)
img = do_paste(img, overlay, (int((iw - ow) / 2), int((ih - oh) / 2)))
return img
def do_rotate(img, rotation):
if not rotation:
return img
try:
rotation = float(rotation)
except ValueError:
rotation = 0
if rotation % 90 == 0:
img = img.rotate(rotation, pil.NEAREST, expand=True)
else:
img = img.rotate(rotation, pil.BICUBIC, expand=True)
return img
def render(data, width, height, force=True, padding=None, overlays=(), overlay_sources=(),
overlay_tints=(), overlay_sizes=None, overlay_positions=None, mask=None, mask_source=None,
center=".5,.5", format=IMAGE_DEFAULT_FORMAT, quality=IMAGE_DEFAULT_QUALITY, fill=None, background=None,
tint=None, pre_rotation=None, post_rotation=None, crop=True, grayscale=False):
"""
Rescale the given image, optionally cropping it to make sure the result image has the specified width and height.
"""
if not isinstance(data, six.string_types):
input_file = BytesIO(data)
else:
input_file = StringIO(data)
img = pil.open(input_file)
if img.mode != "RGBA":
img = img.convert("RGBA")
if width is None:
width = img.size[0]
if height is None:
height = img.size[1]
img = do_rotate(img, pre_rotation)
if crop:
img = resizeCrop(img, width, height, center, force)
else:
img = resizeScale(img, width, height, force)
if grayscale:
img = do_grayscale(img)
do_tint(img, tint)
img = do_fill(img, fill, width, height)
img = do_background(img, background)
do_mask(img, mask, mask_source)
img = do_overlays(img, overlays, overlay_tints, overlay_sources, overlay_sizes, overlay_positions)
img = do_padding(img, padding)
img = do_rotate(img, post_rotation)
tmp = BytesIO()
if not format.upper() in ALPHA_FORMATS:
img = img.convert("RGB")
img.save(tmp, format, quality=quality)
tmp.seek(0)
output_data = tmp.getvalue()
input_file.close()
tmp.close()
return output_data
@lru_cache(maxsize=128)
def image_create_token(parameters):
return "image_token_%s" % hashlib.sha1(parameters.encode("utf8")).hexdigest()
def image_tokenize(session, parameters):
if session:
token = None
for k, v in session.items():
if v == parameters:
token = k
break
if token is None:
token = image_create_token(parameters)
session[token] = parameters
else:
token = image_create_token(parameters)
return token
def image_url(request, parameters, image_field, generate=False):
if generate:
from image import views as image_views
autogen = 'autogen=true' in parameters
image_views.image(request=request, path=str(image_field), token=parameters, autogen=autogen)
image_path = os.path.join(image_tokenize(request.session, parameters), six.text_type(image_field))
return IMAGE_CACHE_STORAGE.url(image_path)

View File

@@ -0,0 +1,44 @@
from django.db import models
from django.db.models.fields.files import FieldFile
from django.core.files import File
def get_video_dimensions(path):
from ffvideo import VideoStream
vs = VideoStream(path)
return (vs.frame_width, vs.frame_height)
class VideoFile(File):
"""
A mixin for use alongside django.core.files.base.File, which provides
additional features for dealing with images.
"""
def _get_width(self):
return self._get_video_dimensions()[0]
width = property(_get_width)
def _get_height(self):
return self._get_video_dimensions()[1]
height = property(_get_height)
def _get_video_dimensions(self):
if not hasattr(self, '_dimensions_cache'):
close = self.closed
self.open()
self._dimensions_cache = get_video_dimensions(self.path)
return self._dimensions_cache
# A video field is exactly a file field with a different signature
class VideoFieldFile(VideoFile, FieldFile):
pass
class VideoField(models.FileField):
attr_class = VideoFieldFile

View File

@@ -0,0 +1,124 @@
# -*- encoding: utf-8 -*-
"""
django-videothumbs
"""
from six import StringIO
from django.conf.global_settings import FILE_UPLOAD_TEMP_DIR
import math
import os
import time
from PIL import Image
TMP_DIR = FILE_UPLOAD_TEMP_DIR or '/tmp/'
def generate_thumb(storage, video_path, thumb_size=None, format='jpg', frames=100):
histogram = []
http_status = 200
name = video_path
path = storage.path(video_path)
if not storage.exists(video_path):
return "", '404'
framemask = "%s%s%s%s" % (TMP_DIR,
name.split('/')[-1].split('.')[0] + str(time.time()),
'.%d.',
format)
# ffmpeg command for grabbing N number of frames
cmd = "/usr/bin/ffmpeg -y -t 00:00:05 -i '%s' '%s'" % (path, framemask)
# make sure that this command worked or return.
if os.system(cmd) != 0:
return "", '500'
# loop through the generated images, open, and generate the image histogram.
for i in range(1, frames + 1):
fname = framemask % i
if not os.path.exists(os.path.join(TMP_DIR, fname)):
break
image = Image.open(fname)
# Convert to RGB if necessary
if image.mode not in ('L', 'RGB'):
image = image.convert('RGB')
# The list of image historgrams
histogram.append(image.histogram())
n = len(histogram)
avg = []
# Get the image average of the first image
for c in range(len(histogram[0])):
ac = 0.0
for i in range(n):
ac = ac + (float(histogram[i][c]) / n)
avg.append(ac)
minn = -1
minRMSE = -1
# Compute the mean squared error
for i in range(1, n + 1):
results = 0.0
num = len(avg)
for j in range(num):
median_error = avg[j] - float(histogram[i - 1][j])
results += (median_error * median_error) / num
rmse = math.sqrt(results)
if minn == -1 or rmse < minRMSE:
minn = i
minRMSE = rmse
file_location = framemask % (minn)
image = Image.open(file_location)
# If you want to generate a square thumbnail
if not thumb_size is None:
thumb_w, thumb_h = thumb_size
if thumb_w == thumb_h:
# quad
xsize, ysize = image.size
# get minimum size
minsize = min(xsize, ysize)
# largest square possible in the image
xnewsize = (xsize - minsize) / 2
ynewsize = (ysize - minsize) / 2
# crop it
image2 = image.crop((xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize))
# load is necessary after crop
image2.load()
# thumbnail of the cropped image (with ANTIALIAS to make it look better)
image2.thumbnail(thumb_size, Image.ANTIALIAS)
else:
# not quad
image2 = image
image2.thumbnail(thumb_size, Image.ANTIALIAS)
else:
image2 = image
io = StringIO()
# PNG and GIF are the same, JPG is JPEG
if format.upper() == 'JPG':
format = 'JPEG'
image2.save(io, format)
# We don't know how many frames we capture. We just captured the first 5 seconds, so keep removing until not found
for i in range(1, 9999):
fname = framemask % i
try:
os.unlink(fname)
except OSError:
break
return io.getvalue(), http_status

View File

@@ -0,0 +1,183 @@
# -*- coding: UTF-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import hashlib
import requests
import six
from django.core.files.base import ContentFile
from encodings.base64_codec import base64_decode
import os
import urllib
import traceback
from django.http import HttpResponse, QueryDict
from django.http.response import Http404
from django.utils import timezone
from django.utils.encoding import force_text
from image.settings import IMAGE_CACHE_HTTP_EXPIRATION, IMAGE_CACHE_ROOT
from image.storage import IMAGE_CACHE_STORAGE, MEDIA_STORAGE, STATIC_STORAGE
from image.utils import IMAGE_DEFAULT_FORMAT, IMAGE_DEFAULT_QUALITY,\
image_create_token, render
from image.videothumbs import generate_thumb
def image(request, path, token, autogen=False):
original_token = token
token = original_token.split("-:-")[0]
has_admin_perm = request.user.has_perm('admin') if request else False
is_admin = False
if ("is_admin=true" in token and request and has_admin_perm) or autogen:
parameters = token
is_admin = True
if autogen:
token = image_create_token(parameters)
else:
parameters = request.session.get(token, token)
cached_image_file = os.path.join(path, token)
now = timezone.now()
expire_offset = timezone.timedelta(seconds=IMAGE_CACHE_HTTP_EXPIRATION)
response = HttpResponse()
response['Content-type'] = 'image/jpeg'
response['Expires'] = (now + expire_offset).strftime("%a, %d %b %Y %T GMT")
response['Last-Modified'] = now.strftime("%a, %d %b %Y %T GMT")
response['Cache-Control'] = 'max-age=3600, must-revalidate'
response.status_code = 200
# If we already have the cache we send it instead of recreating it
if IMAGE_CACHE_STORAGE.exists(cached_image_file):
if autogen:
return 'Already generated'
try:
f = IMAGE_CACHE_STORAGE.open(cached_image_file, "rb")
except IOError:
raise Http404()
response.write(f.read())
f.close()
try:
# Django 2.0 support
modified_time = IMAGE_CACHE_STORAGE.get_modified_time(cached_image_file)
except AttributeError:
modified_time = IMAGE_CACHE_STORAGE.modified_time(cached_image_file)
response['Last-Modified'] = modified_time.strftime("%a, %d %b %Y %T GMT")
return response
if parameters == token and not is_admin:
if has_admin_perm and 'is_admin=true' in path:
# Retrocompatibilidad.
return image(request, original_token, path, autogen=autogen)
return HttpResponse("Forbidden", status=403)
qs = QueryDict(parameters)
file_storage = MEDIA_STORAGE
if qs.get('static', '') == "true":
file_storage = STATIC_STORAGE
format = qs.get('format', IMAGE_DEFAULT_FORMAT)
quality = int(qs.get('quality', IMAGE_DEFAULT_QUALITY))
mask = qs.get('mask', None)
mask_source = qs.get('mask_source', None)
if mask is not None:
format = "PNG"
fill = qs.get('fill', None)
background = qs.get('background', None)
tint = qs.get('tint', None)
center = qs.get('center', ".5,.5")
mode = qs.get('mode', "crop")
enlarge = qs.get('enlarge', "true")
overlays = qs.getlist('overlay')
overlay_sources = qs.getlist('overlay_source')
overlay_tints = qs.getlist('overlay_tint')
overlay_sizes = qs.getlist('overlay_size')
overlay_positions = qs.getlist('overlay_position')
width = qs.get('width', None)
if width:
width = int(width)
height = qs.get('height', None)
if height:
height = int(height)
pre_rotation = qs.get('pre_rotation', None)
post_rotation = qs.get('post_rotation', None)
try:
padding = float(qs.get('padding', None))
except TypeError:
padding = 0.0
grayscale = bool(qs.get('grayscale', False))
if "video" in qs:
data, http_response = generate_thumb(file_storage, force_text(path))
response.status_code = http_response
else:
try:
try:
f = requests.get(qs['url'])
data = f.content
f.close()
except KeyError:
f = file_storage.open(path)
data = f.read()
f.close()
except IOError:
response.status_code = 404
data = ""
if data:
try:
crop = (mode != "scale")
force = (enlarge == "true")
output_data = render(data, width, height, force=force, padding=padding, overlays=overlays,
overlay_sources=overlay_sources, overlay_tints=overlay_tints,
overlay_positions=overlay_positions, overlay_sizes=overlay_sizes, mask=mask,
mask_source=mask_source, center=center, format=format, quality=quality, fill=fill,
background=background, tint=tint, pre_rotation=pre_rotation,
post_rotation=post_rotation, crop=crop, grayscale=grayscale)
except IOError:
traceback.print_exc()
response.status_code = 500
output_data = ""
else:
output_data = data
if response.status_code == 200:
IMAGE_CACHE_STORAGE.save(cached_image_file, output_data, )
if autogen:
return 'Generated ' + six.text_type(response.status_code)
else:
if autogen:
return 'Failed ' + cached_image_file
response.write(output_data)
return response
def crosshair(request):
response = HttpResponse()
response['Content-type'] = 'image/png'
response['Expires'] = 'Fri, 09 Dec 2327 08:34:31 GMT'
response['Last-Modified'] = 'Fri, 24 Sep 2010 11:36:29 GMT'
output, length = base64_decode(b'iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAAwElEQVQoz6WTwY0CMQxFX7DEVjCShUQnc6YCdzCH1EYDboICphb28veA2UULSBHzLpEif9vfcRr/kHQF9jzz3Vr74hWSLpKUmYoIubvMTO6uiFBmqri8FPbeBazAAhwBq3MB1t77c4IH4flNy9T9+Z7g12Nm3iu+Ez4mWMvCFUmKCFVrIywRcasuSe6u8jbC0d3/xGamGs4IZmaSpB3ANE0Ah0HxoeLZAczzDHAaFJ8qfuO0N73z5g37dLfbll/1A+4O0Wm4+ZiPAAAAAElFTkSuQmCC')
response.write(output)
return response