359 lines
14 KiB
Python
Executable File
359 lines
14 KiB
Python
Executable File
#!/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):
|
|
"""
|
|
<dst_dir> set the thumbnailer's storage directory.
|
|
|
|
If <store_on_disk> 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 <size>, a (width,
|
|
height) tupple. Defaults to the 'thumbnail size' preference if not set.
|
|
|
|
If <force_recreation> is True, thumbnails stored on disk
|
|
will always be re-created instead of being re-used.
|
|
|
|
If <archive_support> 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 <filepath>, 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 <filepath>.
|
|
|
|
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.
|
|
<filepath> is the file that was used as source, <pixbuf> is the
|
|
resulting thumbnail. """
|
|
|
|
pass
|
|
|
|
def delete(self, filepath):
|
|
""" Deletes the thumbnail for <filepath> (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 <filepath>, 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 <filepath>, 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 <filepath>. """
|
|
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 <pixbuf> as <thumbpath>, with additional metadata
|
|
from <tEXt_data>. If <thumbpath> 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 <filepath> already exists.
|
|
This function will return False if the thumbnail exists
|
|
and it's mTime doesn't match the mTime of <filepath>,
|
|
it's size is different from the one specified in the thumbnailer,
|
|
or if <force_recreation> 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 <path> to an URI for the thumbnail in <dst_dir>. """
|
|
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 <uri> with <dst_dir>
|
|
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 <files> 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
|