Source

sorl-thumbnail / sorl / thumbnail / templatetags / thumbnail.py

import re
import math
from django.template import Library, Node, VariableDoesNotExist, \
    TemplateSyntaxError
from sorl.thumbnail.main import DjangoThumbnail, get_thumbnail_setting
from sorl.thumbnail.processors import dynamic_import, get_valid_options
from sorl.thumbnail.utils import split_args

register = Library()

size_pat = re.compile(r'(\d+)x(\d+)$')

filesize_formats = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
filesize_long_formats = {
    'k': 'kilo', 'M': 'mega', 'G': 'giga', 'T': 'tera', 'P': 'peta',
    'E': 'exa', 'Z': 'zetta', 'Y': 'yotta',
}

try:
    PROCESSORS = dynamic_import(get_thumbnail_setting('PROCESSORS'))
    VALID_OPTIONS = get_valid_options(PROCESSORS)
except:
    if get_thumbnail_setting('DEBUG'):
        raise
    else:
        PROCESSORS = []
        VALID_OPTIONS = []
TAG_SETTINGS = ['quality']


class ThumbnailNode(Node):
    def __init__(self, source_var, size_var, opts=None,
                 context_name=None, **kwargs):
        self.source_var = source_var
        self.size_var = size_var
        self.opts = opts
        self.context_name = context_name
        self.kwargs = kwargs

    def render(self, context):
        # Note that this isn't a global constant because we need to change the
        # value for tests.
        DEBUG = get_thumbnail_setting('DEBUG')
        try:
            # A file object will be allowed in DjangoThumbnail class
            relative_source = self.source_var.resolve(context)
        except VariableDoesNotExist:
            if DEBUG:
                raise VariableDoesNotExist("Variable '%s' does not exist." %
                        self.source_var)
            else:
                relative_source = None
        try:
            requested_size = self.size_var.resolve(context)
        except VariableDoesNotExist:
            if DEBUG:
                raise TemplateSyntaxError("Size argument '%s' is not a"
                        " valid size nor a valid variable." % self.size_var)
            else:
                requested_size = None
        # Size variable can be either a tuple/list of two integers or a valid
        # string, only the string is checked.
        else:
            if isinstance(requested_size, basestring):
                m = size_pat.match(requested_size)
                if m:
                    requested_size = (int(m.group(1)), int(m.group(2)))
                elif DEBUG:
                    raise TemplateSyntaxError("Variable '%s' was resolved but "
                            "'%s' is not a valid size." %
                            (self.size_var, requested_size))
                else:
                    requested_size = None
        if relative_source is None or requested_size is None:
            thumbnail = ''
        else:
            try:
                kwargs = {}
                for key, value in self.kwargs.items():
                    kwargs[key] = value.resolve(context)
                opts = dict([(k, v and v.resolve(context))
                             for k, v in self.opts.items()])
                thumbnail = DjangoThumbnail(relative_source, requested_size,
                                opts=opts, processors=PROCESSORS, **kwargs)
            except:
                if DEBUG:
                    raise
                else:
                    thumbnail = ''
        # Return the thumbnail class, or put it on the context
        if self.context_name is None:
            return thumbnail
        # We need to get here so we don't have old values in the context
        # variable.
        context[self.context_name] = thumbnail
        return ''


def thumbnail(parser, token):
    """
    Creates a thumbnail of for an ImageField.

    To just output the absolute url to the thumbnail::

        {% thumbnail image 80x80 %}

    After the image path and dimensions, you can put any options::

        {% thumbnail image 80x80 quality=95 crop %}

    To put the DjangoThumbnail class on the context instead of just rendering
    the absolute url, finish the tag with ``as [context_var_name]``::

        {% thumbnail image 80x80 as thumb %}
        {{ thumb.width }} x {{ thumb.height }}
    """
    args = token.split_contents()
    tag = args[0]
    # Check to see if we're setting to a context variable.
    if len(args) > 4 and args[-2] == 'as':
        context_name = args[-1]
        args = args[:-2]
    else:
        context_name = None

    if len(args) < 3:
        raise TemplateSyntaxError("Invalid syntax. Expected "
            "'{%% %s source size [option1 option2 ...] %%}' or "
            "'{%% %s source size [option1 option2 ...] as variable %%}'" %
            (tag, tag))

    # Get the source image path and requested size.
    source_var = parser.compile_filter(args[1])
    # If the size argument was a correct static format, wrap it in quotes so
    # that it is compiled correctly.
    m = size_pat.match(args[2])
    if m:
        args[2] = '"%s"' % args[2]
    size_var = parser.compile_filter(args[2])

    # Get the options.
    args_list = split_args(args[3:]).items()

    # Check the options.
    opts = {}
    kwargs = {} # key,values here override settings and defaults

    for arg, value in args_list:
        value = value and parser.compile_filter(value)
        if arg in TAG_SETTINGS and value is not None:
            kwargs[str(arg)] = value
            continue
        if arg in VALID_OPTIONS:
            opts[arg] = value
        else:
            raise TemplateSyntaxError("'%s' tag received a bad argument: "
                                      "'%s'" % (tag, arg))
    return ThumbnailNode(source_var, size_var, opts=opts,
                         context_name=context_name, **kwargs)


def filesize(bytes, format='auto1024'):
    """
    Returns the number of bytes in either the nearest unit or a specific unit
    (depending on the chosen format method).

    Acceptable formats are:

    auto1024, auto1000
      convert to the nearest unit, appending the abbreviated unit name to the
      string (e.g. '2 KiB' or '2 kB').
      auto1024 is the default format.
    auto1024long, auto1000long
      convert to the nearest multiple of 1024 or 1000, appending the correctly
      pluralized unit name to the string (e.g. '2 kibibytes' or '2 kilobytes').
    kB, MB, GB, TB, PB, EB, ZB or YB
      convert to the exact unit (using multiples of 1000).
    KiB, MiB, GiB, TiB, PiB, EiB, ZiB or YiB
      convert to the exact unit (using multiples of 1024).

    The auto1024 and auto1000 formats return a string, appending the correct
    unit to the value. All other formats return the floating point value.

    If an invalid format is specified, the bytes are returned unchanged.
    """
    format_len = len(format)
    # Check for valid format
    if format_len in (2, 3):
        if format_len == 3 and format[0] == 'K':
            format = 'k%s' % format[1:]
        if not format[-1] == 'B' or format[0] not in filesize_formats:
            return bytes
        if format_len == 3 and format[1] != 'i':
            return bytes
    elif format not in ('auto1024', 'auto1000',
                        'auto1024long', 'auto1000long'):
        return bytes
    # Check for valid bytes
    try:
        bytes = long(bytes)
    except (ValueError, TypeError):
        return bytes

    # Auto multiple of 1000 or 1024
    if format.startswith('auto'):
        if format[4:8] == '1000':
            base = 1000
        else:
            base = 1024
        logarithm = bytes and math.log(bytes, base) or 0
        index = min(int(logarithm) - 1, len(filesize_formats) - 1)
        if index >= 0:
            if base == 1000:
                bytes = bytes and bytes / math.pow(1000, index + 1)
            else:
                bytes = bytes >> (10 * (index))
                bytes = bytes and bytes / 1024.0
            unit = filesize_formats[index]
        else:
            # Change the base to 1000 so the unit will just output 'B' not 'iB'
            base = 1000
            unit = ''
        if bytes >= 10 or ('%.1f' % bytes).endswith('.0'):
            bytes = '%.0f' % bytes
        else:
            bytes = '%.1f' % bytes
        if format.endswith('long'):
            unit = filesize_long_formats.get(unit, '')
            if base == 1024 and unit:
                unit = '%sbi' % unit[:2]
            unit = '%sbyte%s' % (unit, bytes != '1' and 's' or '')
        else:
            unit = '%s%s' % (base == 1024 and unit.upper() or unit,
                             base == 1024 and 'iB' or 'B')

        return '%s %s' % (bytes, unit)

    if bytes == 0:
        return bytes
    base = filesize_formats.index(format[0]) + 1
    # Exact multiple of 1000
    if format_len == 2:
        return bytes / (1000.0 ** base)
    # Exact multiple of 1024
    elif format_len == 3:
        bytes = bytes >> (10 * (base - 1))
        return bytes / 1024.0


register.tag(thumbnail)
register.filter(filesize)