Handle thumbnail creation failure according to the standard.
[rox-lib.git] / ROX-Lib2 / python / rox / thumbnail.py
blobff26cc12c9533a09c5e6526dc0e9752e8e245511
1 """Interface to the thumbnail spec. This provides a functions to look up
2 thumbnails for files and a class which you can extend to generate a thumbnail
3 image for a type of file.
5 The thumbnail standard is at http://jens.triq.net/thumbnail-spec/index.html
6 """
8 import os, sys, errno
9 import tempfile
10 import shutil
12 try:
13 import hashlib
14 def md5hash(s):
15 return hashlib.md5(s).hexdigest()
17 except ImportError:
18 import md5
19 def md5hash(s):
20 return md5.new(s).hexdigest()
22 import rox, rox.basedir, rox.mime
24 def _leaf(fname):
25 path=os.path.abspath(fname)
26 uri='file://'+rox.escape(path)
27 return md5hash(uri)+'.png'
29 def get_path(fname):
30 """Given a file name return the full path of an existing thumbnail
31 image. If no thumbnail image exists, return None"""
33 leaf=_leaf(fname)
34 for sdir in ('normal', 'large'):
35 path=os.path.join(os.environ['HOME'], '.thumbnails',
36 sdir, leaf)
37 if os.access(path, os.R_OK):
38 return path
40 def get_path_save(fname, ttype='normal'):
41 """Given a file name return the full path of the location to store the
42 thumbnail image.
44 ttype should be 'normal' or 'large' to specify the size, or 'fail' when
45 thumbnail creation has failed"""
46 leaf=_leaf(fname)
47 return os.path.join(os.environ['HOME'], '.thumbnails', ttype, leaf)
49 def get_image(fname):
50 """Given a file name return a GdkPixbuf of the thumbnail for that file.
51 If no thumbnail image exists return None."""
52 path=get_path(fname)
53 if not path:
54 return None
56 try:
57 pbuf=rox.g.gdk.pixbuf_new_from_file(path)
58 except:
59 return None
61 # Check validity
62 tsize=int(pbuf.get_option('tEXt::Thumb::Size'))
63 tmtime=float(pbuf.get_option('tEXt::Thumb::MTime'))
64 s=os.stat(fname)
65 if tsize!=int(s.st_size) or int(tmtime)!=int(s.st_mtime):
66 return None
68 return pbuf
71 def get_method(path=None, mtype=None):
72 """Look up the program for generating a thumbnail. Specify either
73 a path to a file or a MIME type.
75 This returns False if there is no defined method to generate the thumbnail,
76 True if the thumbnail would be generated internally using GdkPixbuf, or
77 a string giving the full path to a program called to generate the
78 thumbnail."""
80 if path:
81 mtype=rox.mime.get_type(path)
83 if isinstance(mtype, basestring):
84 mtype=rox.mime.lookup(mtype)
86 if not mtype:
87 return False
89 mthd=rox.basedir.load_first_config('rox.sourceforge.net',
90 'MIME-thumb',
91 '%s_%s' %(mtype.media, mtype.subtype))
93 if mthd:
94 if rox.isappdir(mthd):
95 return os.path.join(mthd, 'AppRun')
96 return mthd
98 for fmt in rox.g.gdk.pixbuf_get_formats():
99 for t in fmt['mime_types']:
100 if t==str(mtype):
101 return True
103 return False
105 def generate(path):
106 """Generate the thumbnail for a file. If a generator for the type of
107 path is not available then None is returned, otherwise an integer
108 which is the exit code of the generation process (0 for success)."""
110 method=get_method(path)
111 if not method:
112 return None
114 if method is True:
115 th=GdkPixbufThumbnailer()
117 th.run(path)
119 return 0
121 outname=get_path_save(path)
122 size=96
124 return os.spawnl(os.P_WAIT, method, method, path, outname, str(size))
126 # Class for thumbnail programs
127 class Thumbnailer:
128 """Base class for programs which generate thumbnails.
130 The method run() creates the thumbnail for a source file. This
131 calls the methods get_image(), process_image() and store_image().
132 process_image() takes the image returned by get_image() and scales it to
133 the correct dimensions then passes it through post_process_image() (which
134 does nothing).
136 You should override the method get_image() to create the image. You can
137 also override post_process_image() if you wish to work on the scaled
138 image."""
140 def __init__(self, name, fname, use_wdir=False, debug=False):
141 """Initialise the thumbnailer.
142 name - name of the program
143 fname - a string to use in generated temp file names
144 use_wdir - if true then use a temp directory to store files
145 debug - if false then suppress most error messages
147 self.name=name
148 self.fname=fname
149 self.use_wdir=use_wdir
150 self.debug=debug
152 self.failed=False
154 def run(self, inname, outname=None, rsize=96):
155 """Generate the thumbnail from the file
156 inname - source file
157 outname - path to store thumbnail image, or None for default location
158 rsize - maximum size of thumbnail (in either axis)
160 if not outname:
161 outname=get_path_save(inname)
163 elif not os.path.isabs(outname):
164 outname=os.path.abspath(outname)
166 if self.use_wdir:
167 self.make_working_dir()
169 try:
170 img=self.get_image(inname, rsize)
171 if img:
172 ow=img.get_width()
173 oh=img.get_height()
174 img=self.process_image(img, rsize)
175 self.store_image(img, inname, outname, ow, oh)
177 else:
178 # Thumbnail creation has failed.
179 self.creation_failed(inname, outname, rsize)
181 except:
182 self.report_exception()
184 if self.use_wdir:
185 self.remove_working_dir()
187 def get_image(self, inname, rsize):
188 """Method you must define for your thumbnailer to do anything"""
189 raise _("Thumbnail not implemented")
191 def process_image(self, img, rsize):
192 """Take the raw image and scale it to the correct size.
193 Returns the result of scaling img and passing it to
194 post_process_image()"""
195 ow=img.get_width()
196 oh=img.get_height()
197 if ow>oh:
198 s=float(rsize)/float(ow)
199 else:
200 s=float(rsize)/float(oh)
201 w=int(s*ow)
202 h=int(s*oh)
204 if w!=ow or h!=oh:
205 img=img.scale_simple(w, h, rox.g.gdk.INTERP_BILINEAR)
207 return self.post_process_image(img, w, h)
209 def post_process_image(self, img, w, h):
210 """Perform some post-processing on the image.
211 img - gdk-pixbuf of the image
212 w - width
213 h - height
214 Return: modified image
215 The default implementation just returns the image unchanged."""
216 return img
218 def store_image(self, img, inname, outname, ow, oh):
219 """Store the thumbnail image it the correct location, adding
220 the extra data required by the thumbnail spec."""
221 s=os.stat(inname)
223 img.save(outname+self.fname, 'png',
224 {'tEXt::Thumb::Image::Width': str(ow),
225 'tEXt::Thumb::Image::Height': str(oh),
226 "tEXt::Thumb::Size": str(s.st_size),
227 "tEXt::Thumb::MTime": str(s.st_mtime),
228 'tEXt::Thumb::URI': rox.escape('file://'+inname),
229 'tEXt::Software': self.name})
230 os.rename(outname+self.fname, outname)
231 self.created=outname
233 def make_working_dir(self):
234 """Create the temporary directory and change into it."""
235 try:
236 self.work_dir=tempfile.mkdtemp()
237 except:
238 self.report_exception()
239 self.work_dir=None
240 return
242 self.old_dir=os.getcwd()
243 os.chdir(self.work_dir)
245 def remove_working_dir(self):
246 """Remove our temporary directory, after changing back to the
247 previous one"""
248 if not self.work_dir:
249 return
251 os.chdir(self.old_dir)
253 try:
254 shutil.rmtree(self.work_dir)
255 except:
256 self.report_exception()
257 self.work_dir=None
259 def creation_failed(self, inname, outname, rsize):
260 """Creation of a thumbnail failed. Stores a dummy file to mark it
261 as per the Thumbnail spec."""
262 self.failed=True
263 s=os.stat(inname)
265 dummy=rox.g.gdk.Pixbuf(rox.g.gdk.COLORSPACE_RGB, False,
266 8, rsize, rsize)
267 outname=get_path_save(inname, ttype=os.path.join('fail',
268 self.fname))
269 d=os.path.dirname(outname)
270 try:
271 os.makedirs(d)
272 except OSError, exc:
273 if exc.errno!=errno.EEXIST:
274 raise
276 dummy.save(outname+self.fname, 'png',
277 {"tEXt::Thumb::Size": str(s.st_size),
278 "tEXt::Thumb::MTime": str(s.st_mtime),
279 'tEXt::Thumb::URI': rox.escape('file://'+inname),
280 'tEXt::Software': self.name})
281 os.rename(outname+self.fname, outname)
282 self.created=outname
285 def report_exception(self):
286 """Report an exception if debug enabled, otherwise ignore it"""
287 if self.debug<1:
288 return
289 rox.report_exception()
291 class GdkPixbufThumbnailer(Thumbnailer):
292 """An example implementation of a Thumbnailer class. It uses GdkPixbuf
293 to generate thumbnails of image files."""
295 def __init__(self):
296 Thumbnailer.__init__(self, 'GdkPixbufThumbnailer', 'pixbuf',
297 False, False)
299 def get_image(self, inname, rsize):
300 if hasattr(rox.g.gdk, 'pixbuf_new_from_file_at_size'):
301 img=rox.g.gdk.pixbuf_new_from_file_at_size(inname, rsize, rsize)
302 else:
303 img=rox.g.gdk.pixbuf_new_from_file(inname)
305 return img