2 from os.path import isfile, isdir, getmtime, dirname, splitext, getsize
3 from tempfile import mkstemp
4 from shutil import copyfile
8 from sorl.thumbnail import defaults
9 from sorl.thumbnail.processors import get_valid_options, dynamic_import
12 class ThumbnailException(Exception):
13 # Stop Django templates from choking if something goes wrong.
14 silent_variable_failure = True
17 class Thumbnail(object):
18 imagemagick_file_types = defaults.IMAGEMAGICK_FILE_TYPES
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
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))
37 self.requested_size = (x, y)
39 self.quality = int(quality)
40 if not 0 < quality <= 100:
42 except (TypeError, ValueError):
43 raise TypeError('Thumbnail received invalid value for quality '
44 'argument: %r' % quality)
47 if processors is None:
48 processors = dynamic_import(defaults.PROCESSORS)
49 self.processors = processors
51 # Handle old list format for opts.
53 if isinstance(opts, (list, tuple)):
54 opts = dict([(opt, None) for opt in opts])
56 # Set Thumbnail opt(ion)s
57 VALID_OPTIONS = get_valid_options(processors)
59 if not opt in VALID_OPTIONS:
60 raise TypeError('Thumbnail received an invalid option: %s'
64 if self.dest is not None:
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.
72 # Ensure dest(ination) attribute is set
74 raise ThumbnailException("No destination filename set.")
76 if not isinstance(self.dest, basestring):
77 # We'll assume dest is a file-like instance if it exists but isn't
80 elif not isfile(self.dest) or (self.source_exists and
81 getmtime(self.source) > getmtime(self.dest)):
83 # Ensure the directory exists
84 directory = dirname(self.dest)
85 if directory and not isdir(directory):
86 os.makedirs(directory)
90 def _check_source_exists(self):
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".
95 if not hasattr(self, '_source_exists'):
96 self._source_exists = (self.source and
97 (not isinstance(self.source, basestring) or
99 return self._source_exists
100 source_exists = property(_check_source_exists)
102 def _get_source_filetype(self):
104 Set the source filetype. First it tries to use magic and
105 if import error it will just use the extension
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.
114 self._source_filetype = splitext(self.source)[1].lower().\
115 replace('.', '').replace('jpeg', 'jpg')
117 m = magic.open(magic.MAGIC_NONE)
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'
127 self._source_filetype = ftype
128 return self._source_filetype
129 source_filetype = property(_get_source_filetype)
131 # data property is the image data of the (generated) thumbnail
133 if not hasattr(self, '_data'):
135 self._data = Image.open(self.dest)
136 except IOError, detail:
137 raise ThumbnailException(detail)
140 def _set_data(self, im):
142 data = property(_get_data, _set_data)
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." %
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)
155 self.source_data = self.source
156 return self._source_data
158 def _set_source_data(self, image):
159 if isinstance(image, Image.Image):
160 self._source_data = image
163 self._source_data = Image.open(image)
164 except IOError, detail:
165 raise ThumbnailException("%s: %s" % (detail, image))
167 raise ThumbnailException("Memory Error: %s" % image)
168 source_data = property(_get_source_data, _set_source_data)
170 def _convert_wvps(self, filename):
174 raise ThumbnailException('wvps requires the Python 2.4 subprocess '
176 tmp = mkstemp('.ps')[1]
178 p = subprocess.Popen((self.wvps_path, filename, tmp),
179 stdout=subprocess.PIPE)
181 except OSError, detail:
183 raise ThumbnailException('wvPS error: %s' % detail)
184 self._convert_imagemagick(tmp)
187 def _convert_imagemagick(self, filename):
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]
197 x, y = self.requested_size
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)
203 except OSError, detail:
205 raise ThumbnailException('ImageMagick error: %s' % detail)
206 self.source_data = tmp
209 def _do_generate(self):
211 Generates the thumbnail image.
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.
216 im = self.source_data
218 for processor in self.processors:
219 im = processor(im, self.requested_size, self.opts)
223 filelike = not isinstance(self.dest, basestring)
225 dest_extension = os.path.splitext(self.dest)[1][1:]
228 dest_extension = None
230 if (self.source_filetype and self.source_filetype == dest_extension and
231 self.source_data == self.data):
232 copyfile(self.source, self.dest)
235 im.save(self.dest, format=format, quality=self.quality,
238 # Try again, without optimization (PIL can't optimize an image
239 # larger than ImageFile.MAXBLOCK, which is 64k by default)
241 im.save(self.dest, format=format, quality=self.quality)
242 except IOError, detail:
243 raise ThumbnailException(detail)
248 # Some helpful methods
250 def _dimension(self, axis):
251 if self.dest is None:
253 return self.data.size[axis]
256 return self._dimension(0)
259 return self._dimension(1)
261 def _get_filesize(self):
262 if self.dest is None:
264 if not hasattr(self, '_filesize'):
265 self._filesize = getsize(self.dest)
266 return self._filesize
267 filesize = property(_get_filesize)
269 def _source_dimension(self, axis):
270 if self.source_filetype in ['pdf', 'doc']:
273 return self.source_data.size[axis]
275 def source_width(self):
276 return self._source_dimension(0)
278 def source_height(self):
279 return self._source_dimension(1)
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)