scripts/comicthumb.py

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