일단 커밋. 오랫동안 커밋을 안해서 꼬였다.
리팩토리 중.
This commit is contained in:
3
.venv/lib/python3.12/site-packages/image/__init__.py
Normal file
3
.venv/lib/python3.12/site-packages/image/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
VERSION = (1, 5, 33)
|
||||
|
||||
default_app_config = 'image.apps.ImageConfig'
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
20
.venv/lib/python3.12/site-packages/image/apps.py
Normal file
20
.venv/lib/python3.12/site-packages/image/apps.py
Normal 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."
|
||||
114
.venv/lib/python3.12/site-packages/image/fields.py
Normal file
114
.venv/lib/python3.12/site-packages/image/fields.py
Normal 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
|
||||
104
.venv/lib/python3.12/site-packages/image/forms.py
Normal file
104
.venv/lib/python3.12/site-packages/image/forms.py
Normal 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
|
||||
25
.venv/lib/python3.12/site-packages/image/misc.py
Normal file
25
.venv/lib/python3.12/site-packages/image/misc.py
Normal 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),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
93
.venv/lib/python3.12/site-packages/image/models.py
Normal file
93
.venv/lib/python3.12/site-packages/image/models.py
Normal 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')
|
||||
25
.venv/lib/python3.12/site-packages/image/settings.py
Normal file
25
.venv/lib/python3.12/site-packages/image/settings.py
Normal 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)
|
||||
98
.venv/lib/python3.12/site-packages/image/storage.py
Normal file
98
.venv/lib/python3.12/site-packages/image/storage.py
Normal 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)()
|
||||
Binary file not shown.
Binary file not shown.
69
.venv/lib/python3.12/site-packages/image/templatetags/img.py
Normal file
69
.venv/lib/python3.12/site-packages/image/templatetags/img.py
Normal 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 + "¢er=" + 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)
|
||||
9
.venv/lib/python3.12/site-packages/image/urls.py
Normal file
9
.venv/lib/python3.12/site-packages/image/urls.py
Normal 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"),
|
||||
]
|
||||
613
.venv/lib/python3.12/site-packages/image/utils.py
Normal file
613
.venv/lib/python3.12/site-packages/image/utils.py
Normal 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)
|
||||
44
.venv/lib/python3.12/site-packages/image/video_field.py
Normal file
44
.venv/lib/python3.12/site-packages/image/video_field.py
Normal 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
|
||||
124
.venv/lib/python3.12/site-packages/image/videothumbs.py
Normal file
124
.venv/lib/python3.12/site-packages/image/videothumbs.py
Normal 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
|
||||
183
.venv/lib/python3.12/site-packages/image/views.py
Normal file
183
.venv/lib/python3.12/site-packages/image/views.py
Normal 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
|
||||
Reference in New Issue
Block a user