24f4d97358f1cda03fa6e911d5025a15845c328f
[redakcja.git] / apps / sorl / thumbnail / base.py
1 import os
2 from os.path import isfile, isdir, getmtime, dirname, splitext, getsize
3 from tempfile import mkstemp
4 from shutil import copyfile
5
6 from PIL import Image
7
8 from sorl.thumbnail import defaults
9 from sorl.thumbnail.processors import get_valid_options, dynamic_import
10
11
12 class ThumbnailException(Exception):
13     # Stop Django templates from choking if something goes wrong.
14     silent_variable_failure = True
15
16
17 class Thumbnail(object):
18     imagemagick_file_types = defaults.IMAGEMAGICK_FILE_TYPES
19
20     def __init__(self, source, requested_size, opts=None, quality=85,
21                  dest=None, convert_path=defaults.CONVERT,
22                  wvps_path=defaults.WVPS, processors=None):
23         # Paths to external commands
24         self.convert_path = convert_path
25         self.wvps_path = wvps_path
26         # Absolute paths to files
27         self.source = source
28         self.dest = dest
29
30         # Thumbnail settings
31         try:
32             x, y = [int(v) for v in requested_size]
33         except (TypeError, ValueError):
34             raise TypeError('Thumbnail received invalid value for size '
35                             'argument: %s' % repr(requested_size))
36         else:
37             self.requested_size = (x, y)
38         try:
39             self.quality = int(quality)
40             if not 0 < quality <= 100:
41                 raise ValueError
42         except (TypeError, ValueError):
43             raise TypeError('Thumbnail received invalid value for quality '
44                             'argument: %r' % quality)
45
46         # Processors
47         if processors is None:
48             processors = dynamic_import(defaults.PROCESSORS)
49         self.processors = processors
50
51         # Handle old list format for opts.
52         opts = opts or {}
53         if isinstance(opts, (list, tuple)):
54             opts = dict([(opt, None) for opt in opts])
55
56         # Set Thumbnail opt(ion)s
57         VALID_OPTIONS = get_valid_options(processors)
58         for opt in opts:
59             if not opt in VALID_OPTIONS:
60                 raise TypeError('Thumbnail received an invalid option: %s'
61                                 % opt)
62         self.opts = opts
63
64         if self.dest is not None:
65             self.generate()
66
67     def generate(self):
68         """
69         Generates the thumbnail if it doesn't exist or if the file date of the
70         source file is newer than that of the thumbnail.
71         """
72         # Ensure dest(ination) attribute is set
73         if not self.dest:
74             raise ThumbnailException("No destination filename set.")
75
76         if not isinstance(self.dest, basestring):
77             # We'll assume dest is a file-like instance if it exists but isn't
78             # a string.
79             self._do_generate()
80         elif not isfile(self.dest) or (self.source_exists and
81             getmtime(self.source) > getmtime(self.dest)):
82
83             # Ensure the directory exists
84             directory = dirname(self.dest)
85             if directory and not isdir(directory):
86                 os.makedirs(directory)
87
88             self._do_generate()
89
90     def _check_source_exists(self):
91         """
92         Ensure the source file exists. If source is not a string then it is
93         assumed to be a file-like instance which "exists".
94         """
95         if not hasattr(self, '_source_exists'):
96             self._source_exists = (self.source and
97                                    (not isinstance(self.source, basestring) or
98                                     isfile(self.source)))
99         return self._source_exists
100     source_exists = property(_check_source_exists)
101
102     def _get_source_filetype(self):
103         """
104         Set the source filetype. First it tries to use magic and
105         if import error it will just use the extension
106         """
107         if not hasattr(self, '_source_filetype'):
108             if not isinstance(self.source, basestring):
109                 # Assuming a file-like object - we won't know it's type.
110                 return None
111             try:
112                 import magic
113             except ImportError:
114                 self._source_filetype = splitext(self.source)[1].lower().\
115                    replace('.', '').replace('jpeg', 'jpg')
116             else:
117                 m = magic.open(magic.MAGIC_NONE)
118                 m.load()
119                 ftype = m.file(self.source)
120                 if ftype.find('Microsoft Office Document') != -1:
121                     self._source_filetype = 'doc'
122                 elif ftype.find('PDF document') != -1:
123                     self._source_filetype = 'pdf'
124                 elif ftype.find('JPEG') != -1:
125                     self._source_filetype = 'jpg'
126                 else:
127                     self._source_filetype = ftype
128         return self._source_filetype
129     source_filetype = property(_get_source_filetype)
130
131     # data property is the image data of the (generated) thumbnail
132     def _get_data(self):
133         if not hasattr(self, '_data'):
134             try:
135                 self._data = Image.open(self.dest)
136             except IOError, detail:
137                 raise ThumbnailException(detail)
138         return self._data
139
140     def _set_data(self, im):
141         self._data = im
142     data = property(_get_data, _set_data)
143
144     # source_data property is the image data from the source file
145     def _get_source_data(self):
146         if not hasattr(self, '_source_data'):
147             if not self.source_exists:
148                 raise ThumbnailException("Source file: '%s' does not exist." %
149                                          self.source)
150             if self.source_filetype == 'doc':
151                 self._convert_wvps(self.source)
152             elif self.source_filetype in self.imagemagick_file_types:
153                 self._convert_imagemagick(self.source)
154             else:
155                 self.source_data = self.source
156         return self._source_data
157
158     def _set_source_data(self, image):
159         if isinstance(image, Image.Image):
160             self._source_data = image
161         else:
162             try:
163                 self._source_data = Image.open(image)
164             except IOError, detail:
165                 raise ThumbnailException("%s: %s" % (detail, image))
166             except MemoryError:
167                 raise ThumbnailException("Memory Error: %s" % image)
168     source_data = property(_get_source_data, _set_source_data)
169
170     def _convert_wvps(self, filename):
171         try:
172             import subprocess
173         except ImportError:
174             raise ThumbnailException('wvps requires the Python 2.4 subprocess '
175                                      'package.')
176         tmp = mkstemp('.ps')[1]
177         try:
178             p = subprocess.Popen((self.wvps_path, filename, tmp),
179                                  stdout=subprocess.PIPE)
180             p.wait()
181         except OSError, detail:
182             os.remove(tmp)
183             raise ThumbnailException('wvPS error: %s' % detail)
184         self._convert_imagemagick(tmp)
185         os.remove(tmp)
186
187     def _convert_imagemagick(self, filename):
188         try:
189             import subprocess
190         except ImportError:
191             raise ThumbnailException('imagemagick requires the Python 2.4 '
192                                      'subprocess package.')
193         tmp = mkstemp('.png')[1]
194         if 'crop' in self.opts or 'autocrop' in self.opts:
195             x, y = [d * 3 for d in self.requested_size]
196         else:
197             x, y = self.requested_size
198         try:
199             p = subprocess.Popen((self.convert_path, '-size', '%sx%s' % (x, y),
200                 '-antialias', '-colorspace', 'rgb', '-format', 'PNG24',
201                 '%s[0]' % filename, tmp), stdout=subprocess.PIPE)
202             p.wait()
203         except OSError, detail:
204             os.remove(tmp)
205             raise ThumbnailException('ImageMagick error: %s' % detail)
206         self.source_data = tmp
207         os.remove(tmp)
208
209     def _do_generate(self):
210         """
211         Generates the thumbnail image.
212
213         This a semi-private method so it isn't directly available to template
214         authors if this object is passed to the template context.
215         """
216         im = self.source_data
217
218         for processor in self.processors:
219             im = processor(im, self.requested_size, self.opts)
220
221         self.data = im
222
223         filelike = not isinstance(self.dest, basestring)
224         if not filelike:
225             dest_extension = os.path.splitext(self.dest)[1][1:]
226             format = None
227         else:
228             dest_extension = None
229             format = 'JPEG'
230         if (self.source_filetype and self.source_filetype == dest_extension and
231                 self.source_data == self.data):
232             copyfile(self.source, self.dest)
233         else:
234             try:
235                 im.save(self.dest, format=format, quality=self.quality,
236                         optimize=1)
237             except IOError:
238                 # Try again, without optimization (PIL can't optimize an image
239                 # larger than ImageFile.MAXBLOCK, which is 64k by default)
240                 try:
241                     im.save(self.dest, format=format, quality=self.quality)
242                 except IOError, detail:
243                     raise ThumbnailException(detail)
244
245         if filelike:
246             self.dest.seek(0)
247
248     # Some helpful methods
249
250     def _dimension(self, axis):
251         if self.dest is None:
252             return None
253         return self.data.size[axis]
254
255     def width(self):
256         return self._dimension(0)
257
258     def height(self):
259         return self._dimension(1)
260
261     def _get_filesize(self):
262         if self.dest is None:
263             return None
264         if not hasattr(self, '_filesize'):
265             self._filesize = getsize(self.dest)
266         return self._filesize
267     filesize = property(_get_filesize)
268
269     def _source_dimension(self, axis):
270         if self.source_filetype in ['pdf', 'doc']:
271             return None
272         else:
273             return self.source_data.size[axis]
274
275     def source_width(self):
276         return self._source_dimension(0)
277
278     def source_height(self):
279         return self._source_dimension(1)
280
281     def _get_source_filesize(self):
282         if not hasattr(self, '_source_filesize'):
283             self._source_filesize = getsize(self.source)
284         return self._source_filesize
285     source_filesize = property(_get_source_filesize)