<commit_before># -*- coding: utf-8 -*-
from __future__ import (absolute_import, unicode_literals)

from io import BytesIO

import os
from django.db.models import signals
from django.db.models.fields.files import ImageField, ImageFileDescriptor, ImageFieldFile
from django.core.files.base import ContentFile
from PIL import Image, ImageOps

from .validators import MinSizeValidator


class StdImageFileDescriptor(ImageFileDescriptor):
    """
    The variation property of the field should be accessible in instance cases

    """

    def __set__(self, instance, value):
        super(StdImageFileDescriptor, self).__set__(instance, value)
        self.field.set_variations(instance)


class StdImageFieldFile(ImageFieldFile):
    """
    Like ImageFieldFile but handles variations.
    """

    def save(self, name, content, save=True):
        super(StdImageFieldFile, self).save(name, content, save)

        for key, variation in self.field.variations.items():
            self.render_and_save_variation(name, content, variation)

    @staticmethod
    def is_smaller(img, variation):
        return img.size[0] > variation['width'] or img.size[1] > variation['height']

    def render_and_save_variation(self, name, content, variation):
        """
        Renders the image variations and saves them to the storage
        """
        content.seek(0)

        resample = variation['resample']

        img = Image.open(content)

        if self.is_smaller(img, variation):
            factor = 1
            while (img.size[0] / factor > 2 * variation['width'] and
                                   img.size[1] * 2 / factor > 2 * variation['height']):
                factor *= 2
            if factor > 1:
                img.thumbnail((int(img.size[0] / factor),
                               int(img.size[1] / factor)), resample=resample)

            if variation['crop']:
                img = ImageOps.fit(img, (variation['width'], variation['height']), method=resample)
            else:
                img.thumbnail((variation['width'], variation['height']), resample=resample)
        variation_name = self.get_variation_name(self.name, variation['name'])
        file_buffer = BytesIO()
        format = self.get_file_extension(name).lower().replace('jpg', 'jpeg')
        img.save(file_buffer, format)
        self.storage.save(variation_name, ContentFile(file_buffer.getvalue()))
        file_buffer.close()

    @classmethod
    def get_variation_name(cls, file_name, variation_name):
        """
        Returns the variation file name based on the model it's field and it's variation
        """
        ext = cls.get_file_extension(file_name)
        path = file_name.rsplit('/', 1)[0]
        file_name = file_name.rsplit('/', 1)[-1].rsplit('.', 1)[0]
        return os.path.join(path, '%s.%s.%s' % (file_name, variation_name, ext))

    @staticmethod
    def get_file_extension(name):
        """
        Returns the file extension.
        """
        filename_split = name.rsplit('.', 1)
        return filename_split[-1]

    def delete(self, save=True):
        for variation in self.field.variations:
            variation_name = self.get_variation_name(self.name, variation)
            self.storage.delete(variation_name)
        super(StdImageFieldFile, self).delete(save)


class StdImageField(ImageField):
    """
    Django field that behaves as ImageField, with some extra features like:
        - Auto resizing
        - Allow image deletion

    :param variations: size variations of the image
    """
    descriptor_class = StdImageFileDescriptor
    attr_class = StdImageFieldFile
    def_variation = {
        'width': float('inf'),
        'height': float('inf'),
        'crop': False,
        'resample': Image.ANTIALIAS
    }

    def __init__(self, verbose_name=None, name=None, variations={},
                 force_min_size=False, *args, **kwargs):
        """
        Standardized ImageField for Django
        Usage: StdImageField(upload_to='PATH', variations={'thumbnail': {"width", "height", "crop", "resample"}})
        :param variations: size variations of the image
        :rtype variations: StdImageField
        """
        self.variations = {}
        self.force_min_size = force_min_size

        for nm, prm in list(variations.items()):
            self.add_variation(nm, prm)

        if self.variations and self.force_min_size:
            self.min_size = max(self.variations.values(), key=lambda x: x["width"])["width"],\
                            max(self.variations.values(), key=lambda x: x["height"])["height"]

        super(StdImageField, self).__init__(verbose_name, name, *args, **kwargs)

    def add_variation(self, name, params):
        if params is None:
            return
        variation = self.def_variation.copy()
        if isinstance(params, (list, tuple)):
            variation.update(dict(zip(("width", "height", "crop"), params)))
        else:
            variation.update(params)
        variation["name"] = name
        self.variations[name] = variation

    def set_variations(self, instance=None, **kwargs):
        """
        Creates a "variation" object as attribute of the ImageField instance.
        Variation attribute will be of the same class as the original image, so
        "path", "url"... properties can be used

        :param instance: FileField
        """
        if getattr(instance, self.name):
            field = getattr(instance, self.name)
            if field._committed:
                for name, variation in list(self.variations.items()):
                    variation_name = self.attr_class.get_variation_name(field.name, variation['name'])
                    variation_field = ImageFieldFile(instance, self, variation_name)
                    setattr(field, name, variation_field)

    def contribute_to_class(self, cls, name):
        """Call methods for generating all operations on specified signals"""
        super(StdImageField, self).contribute_to_class(cls, name)
        signals.post_init.connect(self.set_variations, sender=cls)

    def validate(self, value, model_instance):
        super(StdImageField, self).validate(value, model_instance)
        if self.force_min_size:
            MinSizeValidator(self.min_size[0], self.min_size[1])(value)


try:
    from south.modelsinspector import add_introspection_rules

    add_introspection_rules(
        [([StdImageField], [], {
            "variations": ["variations", {"default": None}],
            "min_size": ["min_size", {"default": None}],
            "max_size": ["max_size", {"default": None}],
        },), ],
        ["^stdimage\.fields\.StdImageField"]
    )
except ImportError:<commit_msg>memory improvements by closeing all files<commit_after># -*- coding: utf-8 -*-
from __future__ import (absolute_import, unicode_literals)

from io import BytesIO

import os
from django.db.models import signals
from django.db.models.fields.files import ImageField, ImageFileDescriptor, ImageFieldFile
from django.core.files.base import ContentFile
from PIL import Image, ImageOps

from .validators import MinSizeValidator


class StdImageFileDescriptor(ImageFileDescriptor):
    """
    The variation property of the field should be accessible in instance cases

    """

    def __set__(self, instance, value):
        super(StdImageFileDescriptor, self).__set__(instance, value)
        self.field.set_variations(instance)


class StdImageFieldFile(ImageFieldFile):
    """
    Like ImageFieldFile but handles variations.
    """

    def save(self, name, content, save=True):
        super(StdImageFieldFile, self).save(name, content, save)

        for key, variation in self.field.variations.items():
            self.render_and_save_variation(name, content, variation)

    @staticmethod
    def is_smaller(img, variation):
        return img.size[0] > variation['width'] or img.size[1] > variation['height']

    def render_and_save_variation(self, name, content, variation):
        """
        Renders the image variations and saves them to the storage
        """
        content.seek(0)

        resample = variation['resample']

        with Image.open(content) as img:

            if self.is_smaller(img, variation):
                factor = 1
                while (img.size[0] / factor > 2 * variation['width'] and
                                       img.size[1] * 2 / factor > 2 * variation['height']):
                    factor *= 2
                if factor > 1:
                    img.thumbnail((int(img.size[0] / factor),
                                   int(img.size[1] / factor)), resample=resample)

                if variation['crop']:
                    img = ImageOps.fit(img, (variation['width'], variation['height']), method=resample)
                else:
                    img.thumbnail((variation['width'], variation['height']), resample=resample)
            variation_name = self.get_variation_name(self.name, variation['name'])
            with BytesIO() as file_buffer:
                format = self.get_file_extension(name).lower().replace('jpg', 'jpeg')
                img.save(file_buffer, format)
                if self.storage.exists(variation_name):
                    self.storage.delete(variation_name)
                self.storage.save(variation_name, ContentFile(file_buffer.getvalue()))
        return variation_name

    @classmethod
    def get_variation_name(cls, file_name, variation_name):
        """
        Returns the variation file name based on the model it's field and it's variation
        """
        ext = cls.get_file_extension(file_name)
        path = file_name.rsplit('/', 1)[0]
        file_name = file_name.rsplit('/', 1)[-1].rsplit('.', 1)[0]
        return os.path.join(path, '%s.%s.%s' % (file_name, variation_name, ext))

    @staticmethod
    def get_file_extension(name):
        """
        Returns the file extension.
        """
        filename_split = name.rsplit('.', 1)
        return filename_split[-1]

    def delete(self, save=True):
        self.delete_variations()
        super(StdImageFieldFile, self).delete(save)

    def delete_variations(self):
        for variation in self.field.variations:
            variation_name = self.get_variation_name(self.name, variation)
            self.storage.delete(variation_name)


class StdImageField(ImageField):
    """
    Django field that behaves as ImageField, with some extra features like:
        - Auto resizing
        - Allow image deletion

    :param variations: size variations of the image
    """
    descriptor_class = StdImageFileDescriptor
    attr_class = StdImageFieldFile
    def_variation = {
        'width': float('inf'),
        'height': float('inf'),
        'crop': False,
        'resample': Image.ANTIALIAS
    }

    def __init__(self, verbose_name=None, name=None, variations={},
                 force_min_size=False, *args, **kwargs):
        """
        Standardized ImageField for Django
        Usage: StdImageField(upload_to='PATH', variations={'thumbnail': {"width", "height", "crop", "resample"}})
        :param variations: size variations of the image
        :rtype variations: StdImageField
        """
        self.variations = {}
        self.force_min_size = force_min_size

        for nm, prm in list(variations.items()):
            self.add_variation(nm, prm)

        if self.variations and self.force_min_size:
            self.min_size = max(self.variations.values(), key=lambda x: x["width"])["width"],\
                            max(self.variations.values(), key=lambda x: x["height"])["height"]

        super(StdImageField, self).__init__(verbose_name, name, *args, **kwargs)

    def add_variation(self, name, params):
        if params is None:
            return
        variation = self.def_variation.copy()
        if isinstance(params, (list, tuple)):
            variation.update(dict(zip(("width", "height", "crop"), params)))
        else:
            variation.update(params)
        variation["name"] = name
        self.variations[name] = variation

    def set_variations(self, instance=None, **kwargs):
        """
        Creates a "variation" object as attribute of the ImageField instance.
        Variation attribute will be of the same class as the original image, so
        "path", "url"... properties can be used

        :param instance: FileField
        """
        if getattr(instance, self.name):
            field = getattr(instance, self.name)
            if field._committed:
                for name, variation in list(self.variations.items()):
                    variation_name = self.attr_class.get_variation_name(field.name, variation['name'])
                    variation_field = ImageFieldFile(instance, self, variation_name)
                    setattr(field, name, variation_field)

    def contribute_to_class(self, cls, name):
        """Call methods for generating all operations on specified signals"""
        super(StdImageField, self).contribute_to_class(cls, name)
        signals.post_init.connect(self.set_variations, sender=cls)

    def validate(self, value, model_instance):
        super(StdImageField, self).validate(value, model_instance)
        if self.force_min_size:
            MinSizeValidator(self.min_size[0], self.min_size[1])(value)


try:
    from south.modelsinspector import add_introspection_rules

    add_introspection_rules(
        [([StdImageField], [], {
            "variations": ["variations", {"default": None}],
            "min_size": ["min_size", {"default": None}],
            "max_size": ["max_size", {"default": None}],
        },), ],
        ["^stdimage\.models\.StdImageField"]
    )
except ImportError:
{'fraction_non_alphanumeric': 0.11640984444606733, 'fraction_numerical': 0.0035054407361425548, 'mean_word_length': 2.5810669456066946, 'pattern_counts': {'":': 12, '<': 3, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 3, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5067373', 'n_tokens_mistral': 3895, 'n_tokens_neox': 3780, 'n_words': 919}