Added custom comicthumb implementation with grid
This commit is contained in:
parent
6cd4bc8ea2
commit
a6c2130926
358
comicthumb.py
Executable file
358
comicthumb.py
Executable file
|
@ -0,0 +1,358 @@
|
|||
#!/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
|
Loading…
Reference in New Issue
Block a user