From a6c2130926607d35e6b0438223c2cf533c972292 Mon Sep 17 00:00:00 2001 From: lanxu Date: Sat, 20 Oct 2018 19:16:55 +0300 Subject: [PATCH] Added custom comicthumb implementation with grid --- comicthumb.py | 358 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100755 comicthumb.py diff --git a/comicthumb.py b/comicthumb.py new file mode 100755 index 0000000..75e03ef --- /dev/null +++ b/comicthumb.py @@ -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): + """ + 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