Ignore compressed statics.
[redakcja.git] / apps / sorl / thumbnail / templatetags / thumbnail.py
1 import re
2 import math
3 from django.template import Library, Node, VariableDoesNotExist, \
4     TemplateSyntaxError
5 from sorl.thumbnail.main import DjangoThumbnail, get_thumbnail_setting
6 from sorl.thumbnail.processors import dynamic_import, get_valid_options
7 from sorl.thumbnail.utils import split_args
8
9 register = Library()
10
11 size_pat = re.compile(r'(\d+)x(\d+)$')
12
13 filesize_formats = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
14 filesize_long_formats = {
15     'k': 'kilo', 'M': 'mega', 'G': 'giga', 'T': 'tera', 'P': 'peta',
16     'E': 'exa', 'Z': 'zetta', 'Y': 'yotta',
17 }
18
19 try:
20     PROCESSORS = dynamic_import(get_thumbnail_setting('PROCESSORS'))
21     VALID_OPTIONS = get_valid_options(PROCESSORS)
22 except:
23     if get_thumbnail_setting('DEBUG'):
24         raise
25     else:
26         PROCESSORS = []
27         VALID_OPTIONS = []
28 TAG_SETTINGS = ['quality']
29
30
31 class ThumbnailNode(Node):
32     def __init__(self, source_var, size_var, opts=None,
33                  context_name=None, **kwargs):
34         self.source_var = source_var
35         self.size_var = size_var
36         self.opts = opts
37         self.context_name = context_name
38         self.kwargs = kwargs
39
40     def render(self, context):
41         # Note that this isn't a global constant because we need to change the
42         # value for tests.
43         DEBUG = get_thumbnail_setting('DEBUG')
44         try:
45             # A file object will be allowed in DjangoThumbnail class
46             relative_source = self.source_var.resolve(context)
47         except VariableDoesNotExist:
48             if DEBUG:
49                 raise VariableDoesNotExist("Variable '%s' does not exist." %
50                         self.source_var)
51             else:
52                 relative_source = None
53         try:
54             requested_size = self.size_var.resolve(context)
55         except VariableDoesNotExist:
56             if DEBUG:
57                 raise TemplateSyntaxError("Size argument '%s' is not a"
58                         " valid size nor a valid variable." % self.size_var)
59             else:
60                 requested_size = None
61         # Size variable can be either a tuple/list of two integers or a valid
62         # string, only the string is checked.
63         else:
64             if isinstance(requested_size, basestring):
65                 m = size_pat.match(requested_size)
66                 if m:
67                     requested_size = (int(m.group(1)), int(m.group(2)))
68                 elif DEBUG:
69                     raise TemplateSyntaxError("Variable '%s' was resolved but "
70                             "'%s' is not a valid size." %
71                             (self.size_var, requested_size))
72                 else:
73                     requested_size = None
74         if relative_source is None or requested_size is None:
75             thumbnail = ''
76         else:
77             try:
78                 kwargs = {}
79                 for key, value in self.kwargs.items():
80                     kwargs[key] = value.resolve(context)
81                 opts = dict([(k, v and v.resolve(context))
82                              for k, v in self.opts.items()])
83                 thumbnail = DjangoThumbnail(relative_source, requested_size,
84                                 opts=opts, processors=PROCESSORS, **kwargs)
85             except:
86                 if DEBUG:
87                     raise
88                 else:
89                     thumbnail = ''
90         # Return the thumbnail class, or put it on the context
91         if self.context_name is None:
92             return thumbnail
93         # We need to get here so we don't have old values in the context
94         # variable.
95         context[self.context_name] = thumbnail
96         return ''
97
98
99 def thumbnail(parser, token):
100     """
101     Creates a thumbnail of for an ImageField.
102
103     To just output the absolute url to the thumbnail::
104
105         {% thumbnail image 80x80 %}
106
107     After the image path and dimensions, you can put any options::
108
109         {% thumbnail image 80x80 quality=95 crop %}
110
111     To put the DjangoThumbnail class on the context instead of just rendering
112     the absolute url, finish the tag with ``as [context_var_name]``::
113
114         {% thumbnail image 80x80 as thumb %}
115         {{ thumb.width }} x {{ thumb.height }}
116     """
117     args = token.split_contents()
118     tag = args[0]
119     # Check to see if we're setting to a context variable.
120     if len(args) > 4 and args[-2] == 'as':
121         context_name = args[-1]
122         args = args[:-2]
123     else:
124         context_name = None
125
126     if len(args) < 3:
127         raise TemplateSyntaxError("Invalid syntax. Expected "
128             "'{%% %s source size [option1 option2 ...] %%}' or "
129             "'{%% %s source size [option1 option2 ...] as variable %%}'" %
130             (tag, tag))
131
132     # Get the source image path and requested size.
133     source_var = parser.compile_filter(args[1])
134     # If the size argument was a correct static format, wrap it in quotes so
135     # that it is compiled correctly.
136     m = size_pat.match(args[2])
137     if m:
138         args[2] = '"%s"' % args[2]
139     size_var = parser.compile_filter(args[2])
140
141     # Get the options.
142     args_list = split_args(args[3:]).items()
143
144     # Check the options.
145     opts = {}
146     kwargs = {} # key,values here override settings and defaults
147
148     for arg, value in args_list:
149         value = value and parser.compile_filter(value)
150         if arg in TAG_SETTINGS and value is not None:
151             kwargs[str(arg)] = value
152             continue
153         if arg in VALID_OPTIONS:
154             opts[arg] = value
155         else:
156             raise TemplateSyntaxError("'%s' tag received a bad argument: "
157                                       "'%s'" % (tag, arg))
158     return ThumbnailNode(source_var, size_var, opts=opts,
159                          context_name=context_name, **kwargs)
160
161
162 def filesize(bytes, format='auto1024'):
163     """
164     Returns the number of bytes in either the nearest unit or a specific unit
165     (depending on the chosen format method).
166
167     Acceptable formats are:
168
169     auto1024, auto1000
170       convert to the nearest unit, appending the abbreviated unit name to the
171       string (e.g. '2 KiB' or '2 kB').
172       auto1024 is the default format.
173     auto1024long, auto1000long
174       convert to the nearest multiple of 1024 or 1000, appending the correctly
175       pluralized unit name to the string (e.g. '2 kibibytes' or '2 kilobytes').
176     kB, MB, GB, TB, PB, EB, ZB or YB
177       convert to the exact unit (using multiples of 1000).
178     KiB, MiB, GiB, TiB, PiB, EiB, ZiB or YiB
179       convert to the exact unit (using multiples of 1024).
180
181     The auto1024 and auto1000 formats return a string, appending the correct
182     unit to the value. All other formats return the floating point value.
183
184     If an invalid format is specified, the bytes are returned unchanged.
185     """
186     format_len = len(format)
187     # Check for valid format
188     if format_len in (2, 3):
189         if format_len == 3 and format[0] == 'K':
190             format = 'k%s' % format[1:]
191         if not format[-1] == 'B' or format[0] not in filesize_formats:
192             return bytes
193         if format_len == 3 and format[1] != 'i':
194             return bytes
195     elif format not in ('auto1024', 'auto1000',
196                         'auto1024long', 'auto1000long'):
197         return bytes
198     # Check for valid bytes
199     try:
200         bytes = long(bytes)
201     except (ValueError, TypeError):
202         return bytes
203
204     # Auto multiple of 1000 or 1024
205     if format.startswith('auto'):
206         if format[4:8] == '1000':
207             base = 1000
208         else:
209             base = 1024
210         logarithm = bytes and math.log(bytes, base) or 0
211         index = min(int(logarithm) - 1, len(filesize_formats) - 1)
212         if index >= 0:
213             if base == 1000:
214                 bytes = bytes and bytes / math.pow(1000, index + 1)
215             else:
216                 bytes = bytes >> (10 * (index))
217                 bytes = bytes and bytes / 1024.0
218             unit = filesize_formats[index]
219         else:
220             # Change the base to 1000 so the unit will just output 'B' not 'iB'
221             base = 1000
222             unit = ''
223         if bytes >= 10 or ('%.1f' % bytes).endswith('.0'):
224             bytes = '%.0f' % bytes
225         else:
226             bytes = '%.1f' % bytes
227         if format.endswith('long'):
228             unit = filesize_long_formats.get(unit, '')
229             if base == 1024 and unit:
230                 unit = '%sbi' % unit[:2]
231             unit = '%sbyte%s' % (unit, bytes != '1' and 's' or '')
232         else:
233             unit = '%s%s' % (base == 1024 and unit.upper() or unit,
234                              base == 1024 and 'iB' or 'B')
235
236         return '%s %s' % (bytes, unit)
237
238     if bytes == 0:
239         return bytes
240     base = filesize_formats.index(format[0]) + 1
241     # Exact multiple of 1000
242     if format_len == 2:
243         return bytes / (1000.0 ** base)
244     # Exact multiple of 1024
245     elif format_len == 3:
246         bytes = bytes >> (10 * (base - 1))
247         return bytes / 1024.0
248
249
250 register.tag(thumbnail)
251 register.filter(filesize)