#!/usr/bin/env python2 """thumbnail.py - Thumbnail module for MComix implementing (most of) the freedesktop.org "standard" at http://jens.triq.net/thumbnail-spec/ """ import sys import urllib2 import os import re import shutil import tempfile import mimetypes import threading import itertools import traceback import PIL.Image as Image import gtk from urllib import pathname2url try: # The md5 module is deprecated as of Python 2.5, replaced by hashlib. from hashlib import md5 except ImportError: from md5 import new as md5 from mcomix.preferences import prefs from mcomix import archive_extractor from mcomix import constants from mcomix import archive_tools from mcomix import tools from mcomix import image_tools from mcomix import portability from mcomix import i18n from mcomix import callback from mcomix import log class Thumbnailer(object): """ The Thumbnailer class is responsible for managing MComix internal thumbnail creation. Depending on its settings, it either stores thumbnails on disk and retrieves them later, or simply creates new thumbnails each time it is called. """ def __init__(self, dst_dir=constants.THUMBNAIL_PATH, store_on_disk=None, size=None, force_recreation=False, archive_support=False, multiple_previews=False): """ set the thumbnailer's storage directory. If on disk is True, it changes the thumbnailer's behaviour to store files on disk, or just create new thumbnails each time it was called when set to False. Defaults to the 'create thumbnails' preference if not set. The dimensions for the created thumbnails is set by , a (width, height) tupple. Defaults to the 'thumbnail size' preference if not set. If is True, thumbnails stored on disk will always be re-created instead of being re-used. If is True, support for archive thumbnail creation (based on cover detection) is enabled. Otherwise, only image files are supported. """ self.dst_dir = dst_dir if store_on_disk is None: self.store_on_disk = prefs['create thumbnails'] else: self.store_on_disk = store_on_disk if size is None: self.width = self.height = prefs['thumbnail size'] self.default_sizes = True else: self.width, self.height = size self.default_sizes = False self.force_recreation = force_recreation self.archive_support = archive_support self.multiple_previews = multiple_previews def thumbnail(self, filepath, async=False): """ Returns a thumbnail pixbuf for , transparently handling both normal image files and archives. If a thumbnail file already exists, it is re-used. Otherwise, a new thumbnail is created from . Returns None if thumbnail creation failed, or if the thumbnail creation is run asynchrounosly. """ # Update width and height from preferences if they haven't been set explicitly if self.default_sizes: self.width = prefs['thumbnail size'] self.height = prefs['thumbnail size'] if self._thumbnail_exists(filepath): thumbpath = self._path_to_thumbpath(filepath) pixbuf = image_tools.load_pixbuf(thumbpath) self.thumbnail_finished(filepath, pixbuf) return pixbuf else: if async: thread = threading.Thread(target=self._create_thumbnail, args=(filepath,)) thread.name += '-thumbnailer' thread.setDaemon(True) thread.start() return None else: return self._create_thumbnail(filepath) @callback.Callback def thumbnail_finished(self, filepath, pixbuf): """ Called every time a thumbnail has been completed. is the file that was used as source, is the resulting thumbnail. """ pass def delete(self, filepath): """ Deletes the thumbnail for (if it exists) """ thumbpath = self._path_to_thumbpath(filepath) if os.path.isfile(thumbpath): try: os.remove(thumbpath) except IOError, error: log.error(_("! Could not remove file \"%s\""), thumbpath) log.error(error) def _create_thumbnail_pixbuf(self, filepath): """ Creates a thumbnail pixbuf from , and returns it as a tuple along with a file metadata dictionary: (pixbuf, tEXt_data) """ if self.archive_support: mime = archive_tools.archive_mime_type(filepath) else: mime = None if mime is not None: cleanup = [] try: tmpdir = tempfile.mkdtemp(prefix=u'mcomix_archive_thumb.') cleanup.append(lambda: shutil.rmtree(tmpdir, True)) archive = archive_tools.get_recursive_archive_handler(filepath, tmpdir, type=mime) if archive is None: return None, None cleanup.append(archive.close) files = archive.list_contents() wanted = self._guess_cover(files) if wanted is None: return None, None archive.extract(wanted, tmpdir) image_path = os.path.join(tmpdir, wanted) if not os.path.isfile(image_path): return None, None if self.multiple_previews: number_of_files = len(files) pixbuf1 = image_tools.load_pixbuf_size(image_path, self.width/2, self.height/2) """ Extract 3 images """ jump = number_of_files / 4 frames = [jump*1, jump*2, jump*3] pixbufs = [pixbuf1] has_alpha = False for frame in frames: archive.extract(files[frame], tmpdir) image_path = os.path.join(tmpdir, files[frame]) if not os.path.isfile(image_path): return None, None pixbuf_temp = image_tools.load_pixbuf_size(image_path, self.width/2, self.height/2) pixbufs.append(pixbuf_temp) if pixbuf_temp.get_property('has-alpha'): has_alpha = True bits_per_sample = 8 has_alpha = True pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, has_alpha, bits_per_sample, self.width, self.height) pixbuf.fill(0x00000000) offsets = [[0, 0], [self.width/2, 0], [0, self.height/2], [self.width/2, self.height/2]] for buf in pixbufs: idx = pixbufs.index(buf) src_w = buf.get_property('width') src_h = buf.get_property('height') center_x = (self.width / 2 - src_w) / 2 center_y = (self.height / 2 - src_h) / 2 buf.copy_area(0, 0, src_w, src_h, pixbuf, center_x + offsets[idx][0], center_y + offsets[idx][1]) #pixbufs[1].copy_area(0, 0, pixbufs[1].get_property('width'), pixbufs[1].get_property('height'), pixbuf, self.width/2, 0) #pixbufs[2].copy_area(0, 0, pixbufs[2].get_property('width'), pixbufs[2].get_property('height'), pixbuf, 0, self.height/2) #pixbufs[3].copy_area(0, 0, pixbufs[3].get_property('width'), pixbufs[3].get_property('height'), pixbuf, self.width/2, self.height/2) else: pixbuf = image_tools.load_pixbuf_size(image_path, self.width, self.height) if self.store_on_disk: tEXt_data = self._get_text_data(image_path) # Use the archive's mTime instead of the extracted file's mtime tEXt_data['tEXt::Thumb::MTime'] = str(long(os.stat(filepath).st_mtime)) else: tEXt_data = None return pixbuf, tEXt_data finally: for fn in reversed(cleanup): fn() elif image_tools.is_image_file(filepath): pixbuf = image_tools.load_pixbuf_size(filepath, self.width, self.height) if self.store_on_disk: tEXt_data = self._get_text_data(filepath) else: tEXt_data = None return pixbuf, tEXt_data else: return None, None def _create_thumbnail(self, filepath): """ Creates the thumbnail pixbuf for , and saves the pixbuf to disk if necessary. Returns the created pixbuf, or None, if creation failed. """ pixbuf, tEXt_data = self._create_thumbnail_pixbuf(filepath) self.thumbnail_finished(filepath, pixbuf) if pixbuf and self.store_on_disk: thumbpath = self._path_to_thumbpath(filepath) self._save_thumbnail(pixbuf, thumbpath, tEXt_data) return pixbuf def _get_text_data(self, filepath): """ Creates a tEXt dictionary for . """ mime = mimetypes.guess_type(filepath)[0] or "unknown/mime" uri = portability.uri_prefix() + pathname2url(i18n.to_utf8(os.path.normpath(filepath))) stat = os.stat(filepath) # MTime could be floating point number, so convert to long first to have a fixed point number mtime = str(long(stat.st_mtime)) size = str(stat.st_size) format, (width, height), providers = image_tools.get_image_info(filepath) return { 'tEXt::Thumb::URI': uri, 'tEXt::Thumb::MTime': mtime, 'tEXt::Thumb::Size': size, 'tEXt::Thumb::Mimetype': mime, 'tEXt::Thumb::Image::Width': str(width), 'tEXt::Thumb::Image::Height': str(height), 'tEXt::Software': 'MComix %s' % constants.VERSION } def _save_thumbnail(self, pixbuf, thumbpath, tEXt_data): """ Saves as , with additional metadata from . If already exists, it is overwritten. """ try: directory = os.path.dirname(thumbpath) if not os.path.isdir(directory): os.makedirs(directory, 0700) if os.path.isfile(thumbpath): os.remove(thumbpath) pixbuf.save(thumbpath, 'png', tEXt_data) os.chmod(thumbpath, 0600) except Exception, ex: log.warning( _('! Could not save thumbnail "%(thumbpath)s": %(error)s'), { 'thumbpath' : thumbpath, 'error' : ex } ) def _thumbnail_exists(self, filepath): """ Checks if the thumbnail for already exists. This function will return False if the thumbnail exists and it's mTime doesn't match the mTime of , it's size is different from the one specified in the thumbnailer, or if is True. """ if not self.force_recreation: thumbpath = self._path_to_thumbpath(filepath) if os.path.isfile(thumbpath): # Check the thumbnail's stored mTime try: img = Image.open(thumbpath) except IOError: return False info = img.info stored_mtime = long(info['Thumb::MTime']) # The source file might no longer exist file_mtime = os.path.isfile(filepath) and long(os.stat(filepath).st_mtime) or stored_mtime return stored_mtime == file_mtime and \ max(*img.size) == max(self.width, self.height) else: return False else: return False def _path_to_thumbpath(self, filepath): """ Converts to an URI for the thumbnail in . """ uri = portability.uri_prefix() + pathname2url(i18n.to_utf8(os.path.normpath(filepath))) return self._uri_to_thumbpath(uri) def _uri_to_thumbpath(self, uri): """ Return the full path to the thumbnail for with being the base thumbnail directory. """ md5hash = md5(uri).hexdigest() thumbpath = os.path.join(self.dst_dir, md5hash + '.png') return thumbpath def _guess_cover(self, files): """Return the filename within that is the most likely to be the cover of an archive using some simple heuristics. """ # Ignore MacOSX meta files. files = itertools.ifilter(lambda filename: u'__MACOSX' not in os.path.normpath(filename).split(os.sep), files) # Ignore credit files if possible. files = itertools.ifilter(lambda filename: u'credit' not in os.path.split(filename)[1].lower(), files) images = list(itertools.ifilter(image_tools.is_image_file, files)) tools.alphanumeric_sort(images) front_re = re.compile('(cover|front)', re.I) candidates = filter(front_re.search, images) candidates = [c for c in candidates if 'back' not in c.lower()] if candidates: return candidates[0] if images: return images[0] return None if __name__ == '__main__': argv = portability.get_commandline_args() try: in_path = argv[0] out_path = argv[1] if len(argv) == 3: size = int(argv[2]) else: size = 128 except: print __doc__ sys.exit(1) if in_path.startswith('file://'): in_path = urllib2.unquote(in_path[7:]) thumbnailer = Thumbnailer(force_recreation=True, archive_support=True, store_on_disk=False, size=(size, size), multiple_previews=True) thumb = thumbnailer.thumbnail(in_path) thumb.save(out_path, 'png') sys.exit(0) # vim: expandtab:sw=4:ts=4