diff --git a/models.py b/models.py index a291a91..36dba60 100644 --- a/models.py +++ b/models.py @@ -217,28 +217,28 @@ class Item(models.Model): sil_c = models.TextField(null=True, blank=True) MODEL_TO_MEDIA_DATA = { - media.metadata.KEY_SIZE: 'size_c', - media.metadata.KEY_TIME: 'datetime_c', - media.metadata.KEY_EXPOSURE_PROGRAM: 'exposure_program_c', - media.metadata.KEY_EXPOSURE_TIME: 'exposure_time_c', - media.metadata.KEY_FLASH: 'flash_c', - media.metadata.KEY_APERTURE: 'f_number_c', - media.metadata.KEY_FOCAL_LENGTH: 'focal_length_c', - media.metadata.KEY_GPS: {'lon': 'lon_c', 'lat': 'lat_c'}, - media.metadata.KEY_HEIGHT: 'height_c', - media.metadata.KEY_ISO: 'iso_c', - media.metadata.KEY_CAMERA: 'camera_c', - media.metadata.KEY_ORIENTATION: 'orientation_c', - media.metadata.KEY_WIDTH: 'width_c', - media.metadata.KEY_DURATION: 'duration_c', - media.metadata.KEY_RATIO: 'ratio_c', - media.metadata.KEY_ALBUM: 'album_c', - media.metadata.KEY_ARTIST: 'artist_c', - media.metadata.KEY_BITRATE: 'bitrate_c', - media.metadata.KEY_GENRE: 'genre_c', - media.metadata.KEY_TITLE: 'title_c', - media.metadata.KEY_TRACK: 'track_c', - media.metadata.KEY_YEAR: 'year_c', + media.KEY_SIZE: 'size_c', + media.KEY_TIME: 'datetime_c', + media.KEY_EXPOSURE_PROGRAM: 'exposure_program_c', + media.KEY_EXPOSURE_TIME: 'exposure_time_c', + media.KEY_FLASH: 'flash_c', + media.KEY_APERTURE: 'f_number_c', + media.KEY_FOCAL_LENGTH: 'focal_length_c', + media.KEY_GPS: {'lon': 'lon_c', 'lat': 'lat_c'}, + media.KEY_HEIGHT: 'height_c', + media.KEY_ISO: 'iso_c', + media.KEY_CAMERA: 'camera_c', + media.KEY_ORIENTATION: 'orientation_c', + media.KEY_WIDTH: 'width_c', + media.KEY_DURATION: 'duration_c', + media.KEY_RATIO: 'ratio_c', + media.KEY_ALBUM: 'album_c', + media.KEY_ARTIST: 'artist_c', + media.KEY_BITRATE: 'bitrate_c', + media.KEY_GENRE: 'genre_c', + media.KEY_TITLE: 'title_c', + media.KEY_TRACK: 'track_c', + media.KEY_YEAR: 'year_c', } def __init__(self, *args, **kwargs): @@ -425,14 +425,14 @@ class Item(models.Model): data = media.get_media_data(full_path) or {} for key in self.MODEL_TO_MEDIA_DATA: value = data.get(key) - if key == media.metadata.KEY_GPS: # Split GPS data in lon and lat + if key == media.KEY_GPS: # Split GPS data in lon and lat for k in self.MODEL_TO_MEDIA_DATA[key]: value_k = value[k] if value is not None else None setattr(self, self.MODEL_TO_MEDIA_DATA[key][k], value_k) else: if value is not None: - if key == media.metadata.KEY_TIME: # convert time to datetime - if data.get(media.metadata.KEY_TIME_IS_SUBSTITUTION) and self.type == TYPE_IMAGE: # don't use time substitution for images + if key == media.KEY_TIME: # convert time to datetime + if data.get(media.KEY_TIME_IS_SUBSTITUTION) and self.type == TYPE_IMAGE: # don't use time substitution for images value = None else: value = datetime.datetime.fromtimestamp(value, tz=datetime.timezone.utc) diff --git a/signals.py b/signals.py index fa48a72..7df5442 100644 --- a/signals.py +++ b/signals.py @@ -5,7 +5,7 @@ import logging from .models import Item, Tag from .search import load_index, delete_item, update_item import shutil -from .views.image import base_item +from .views.xnail import base_item # Get a logger instance clogger = logging.getLogger("CACHING") diff --git a/views/__init__.py b/views/__init__.py index 6c3a456..fab3948 100644 --- a/views/__init__.py +++ b/views/__init__.py @@ -9,8 +9,9 @@ from django.utils.translation import gettext as _ from ..forms import TagForm import fstools from ..help import help_pages -from .image import get_image_instance, other +from .xnail import get_image_instance, other from .infoviews import get_wrapper_instance as get_infoview_wrapper_instance +import media import mimetypes from ..models import get_item_by_rel_path, get_item_type, TYPE_IMAGE, Tag, Item import os @@ -23,7 +24,6 @@ from users.forms import UserProfileFormLanguageOnly from .userviews import get_wrapper_instance as get_userview_wrapper_instance import zipfile from pygal.views.userviews import query_view -from pygal.views.image import mm_image def pygal_item_does_not_exist(request, context): @@ -177,7 +177,7 @@ def pygal_item(request, responsetype, rel_path): data = open(full_path, 'rb').read() return HttpResponse(data, content_type=mime_type) else: - im = mm_image(os.path.join(os.path.dirname(__file__), 'forbidden.png')) + im = media.image(os.path.join(os.path.dirname(__file__), 'forbidden.png')) if responsetype == pygal.RESP_TYPE_THUMBNAIL: im.resize(int(pygal.get_thumbnail_size(request) * .75)) return HttpResponse(im.image_data(), content_type='image/png') diff --git a/views/image.py b/views/image.py deleted file mode 100644 index 98fd55a..0000000 --- a/views/image.py +++ /dev/null @@ -1,284 +0,0 @@ -from django.conf import settings -import fstools -import io -import logging -import mimetypes -from ..models import get_item_type, TYPE_IMAGE, TYPE_VIDEO -import os -from PIL import Image, ImageEnhance, ExifTags -import platform -import pygal -import subprocess - -# Get a logger instance -logger = logging.getLogger("CACHING") - - -def get_image_class(full_path): - return { - TYPE_IMAGE: image, - TYPE_VIDEO: video, - }.get(get_item_type(full_path), other) - - -def get_image_instance(full_path, request): - return get_image_class(full_path)(full_path, request) - - -class mm_image(object): - JOIN_TOP_LEFT = 1 - JOIN_TOP_RIGHT = 2 - JOIN_BOT_LEFT = 3 - JOIN_BOT_RIGHT = 4 - JOIN_CENTER = 5 - - def __init__(self, imagepath_handle_image): - self.imagepath_handle_image = imagepath_handle_image - self.__image__ = None - # - - def __init_image__(self): - if self.__image__ is None: - if type(self.imagepath_handle_image) is mm_image: - self.__image__ = self.imagepath_handle_image.__image__ - elif type(self.imagepath_handle_image) is Image.Image: - self.__image__ = self.imagepath_handle_image - else: - self.__image__ = Image.open(self.imagepath_handle_image) - - def orientation(self): - self.__init_image__() - exif_tags = dict((v, k) for k, v in ExifTags.TAGS.items()) - try: - return dict(self.__image__._getexif().items())[exif_tags['Orientation']] - except AttributeError: - return 1 - except KeyError: - return 1 - - def copy(self): - self.__init_image__() - return mm_image(self.__image__.copy()) - - def save(self, *args, **kwargs): - self.__init_image__() - im = self.__image__.copy().convert('RGB') - im.save(*args, **kwargs) - - def resize(self, max_size): - self.__init_image__() - # - # resize - # - x, y = self.__image__.size - xy_max = max(x, y) - self.__image__ = self.__image__.resize((int(x * float(max_size) / xy_max), int(y * float(max_size) / xy_max)), Image.NEAREST).rotate(0) - - def rotate_by_orientation(self, orientation): - self.__init_image__() - # - # rotate - # - angle = {3: 180, 6: 270, 8: 90}.get(orientation) - if angle is not None: - self.__image__ = self.__image__.rotate(angle, expand=True) - - def __rgba_copy__(self): - self.__init_image__() - if self.__image__.mode != 'RGBA': - return self.__image__.convert('RGBA') - else: - return self.__image__.copy() - - def join(self, image, joint_pos=JOIN_TOP_RIGHT, opacity=0.7): - """ - This joins another picture to this one. - - :param picture_edit picture: The picture to be joint. - :param joint_pos: The position of picture in this picture. See also self.JOIN_* - :param float opacity: The opacity of picture when joint (value between 0 and 1). - - .. note:: - joint_pos makes only sense if picture is smaller than this picture. - """ - self.__init_image__() - # - im2 = image.__rgba_copy__() - # change opacity of im2 - alpha = im2.split()[3] - alpha = ImageEnhance.Brightness(alpha).enhance(opacity) - im2.putalpha(alpha) - - self.__image__ = self.__rgba_copy__() - - # create a transparent layer - layer = Image.new('RGBA', self.__image__.size, (0, 0, 0, 0)) - # draw im2 in layer - if joint_pos == self.JOIN_TOP_LEFT: - layer.paste(im2, (0, 0)) - elif joint_pos == self.JOIN_TOP_RIGHT: - layer.paste(im2, ((self.__image__.size[0] - im2.size[0]), 0)) - elif joint_pos == self.JOIN_BOT_LEFT: - layer.paste(im2, (0, (self.__image__.size[1] - im2.size[1]))) - elif joint_pos == self.JOIN_BOT_RIGHT: - layer.paste(im2, ((self.__image__.size[0] - im2.size[0]), (self.__image__.size[1] - im2.size[1]))) - elif joint_pos == self.JOIN_CENTER: - layer.paste(im2, (int((self.__image__.size[0] - im2.size[0]) / 2), int((self.__image__.size[1] - im2.size[1]) / 2))) - - self.__image__ = Image.composite(layer, self.__image__, layer) - - def image_data(self): - self.__init_image__() - # - # create return value - # - im = self.__image__.copy().convert('RGB') - output = io.BytesIO() - im.save(output, format='JPEG') - return output.getvalue() - - -class mm_video(object): - def __init__(self, full_path): - self.full_path = full_path - - def image(self): - if platform.system() == 'Linux': - cmd = 'ffmpeg -ss 0.5 -i "' + self.full_path + '" -vframes 1 -f image2pipe pipe:1 2> /dev/null' - else: - cmd = 'ffmpeg -ss 0.5 -i "' + self.full_path + '" -vframes 1 -f image2pipe pipe:1 2> NULL' - data = subprocess.check_output(cmd, shell=True) - ffmpeg_handle = io.BytesIO(data) - im = Image.open(ffmpeg_handle) - return mm_image(im.copy()) - - -class base_item(object): - MIME_TYPES = { - '.ada': 'text/x/adasrc', - '.hex': 'text/x/hex', - '.jpg': 'image/x/generic', - '.jpeg': 'image/x/generic', - '.jpe': 'image/x/generic', - '.png': 'image/x/generic', - '.tif': 'image/x/generic', - '.tiff': 'image/x/generic', - '.gif': 'image/x/generic', - '.avi': 'video/x/generic', - '.mpg': 'video/x/generic', - '.mpeg': 'video/x/generic', - '.mpe': 'video/x/generic', - '.mov': 'video/x/generic', - '.qt': 'video/x/generic', - '.mp4': 'video/x/generic', - '.webm': 'video/x/generic', - '.ogv': 'video/x/generic', - '.flv': 'video/x/generic', - '.3gp': 'video/x/generic', - } - - def __init__(self, full_path, request): - self.full_path = full_path - self.request = request - self.rel_path = pygal.get_rel_path(full_path) - # - ext = os.path.splitext(self.full_path)[1].lower() - self.mime_type = self.MIME_TYPES.get(ext, mimetypes.types_map.get(ext, 'unknown')) - - def __cache_image_folder__(self, rel_path): - return os.path.join(settings.XNAIL_ROOT, rel_path.replace('_', '__').replace('/', '_')) - - def __cache_image_name__(self, max_size): - filename = '%04d_%02x_%s.jpg' % (max_size, self.XNAIL_VERSION_NUMBER, fstools.uid(self.full_path, None)) - return os.path.join(self.__cache_image_folder__(self.rel_path), filename) - - def __delete_cache_image__(self, max_size): - for fn in fstools.filelist(self.__cache_image_folder__(self.rel_path), '%04d*' % max_size): - try: - os.remove(fn) - except OSError: - pass # possibly file is already removed by another process - - -class image(base_item, mm_image): - XNAIL_VERSION_NUMBER = 1 - - def __init__(self, *args, **kwargs): - base_item.__init__(self, *args, **kwargs) - mm_image.__init__(self, self.full_path) - self.mime_type_xnails = mimetypes.types_map.get('.jpg', 'unknown') - - def get_resized_image_data(self, max_size): - cache_filename = self.__cache_image_name__(max_size) - if not os.path.exists(cache_filename): - logger.info('Creating xnail-%d for %s', max_size, self.rel_path) - self.__delete_cache_image__(max_size) - im = self.copy() - im.resize(max_size) - im.rotate_by_orientation(self.orientation()) - # - # create cache file - # - fstools.mkdir(os.path.dirname(cache_filename)) - with open(cache_filename, 'wb') as fh: - im.save(fh, format='JPEG') - # - return im.image_data() - else: - return open(cache_filename, 'rb').read() - - def thumbnail_picture(self): - return self.get_resized_image_data(pygal.get_thumbnail_max_size(self.request)) - - def webnail_picture(self): - return self.get_resized_image_data(pygal.get_webnail_size(self.request)) - - -class video(base_item, mm_video): - XNAIL_VERSION_NUMBER = 1 - - def __init__(self, *args, **kwargs): - base_item.__init__(self, *args, **kwargs) - mm_video.__init__(self, self.full_path) - self.mime_type_xnails = mimetypes.types_map.get('.jpg', 'unknown') - - def get_resized_image_data(self, max_size): - cache_filename = self.__cache_image_name__(max_size) - if not os.path.exists(cache_filename): - logger.info('Creating xnail-%d for %s', max_size, self.rel_path) - self.__delete_cache_image__(max_size) - im = self.image() - im.resize(max_size) - overlay = mm_image(os.path.join(os.path.dirname(__file__), 'video.png')) - im.join(overlay) - # - # create cache file - # - fstools.mkdir(os.path.dirname(cache_filename)) - with open(cache_filename, 'wb') as fh: - im.save(fh, format='JPEG') - # - return im.image_data() - else: - return open(cache_filename, 'rb').read() - - def thumbnail_picture(self): - return image.thumbnail_picture(self) - - def webnail_picture(self): - return image.webnail_picture(self) - - -class other(base_item): - def __init__(self, *args, **kwargs): - base_item.__init__(self, *args, **kwargs) - self.mime_type_xnails = mimetypes.types_map.get('.png', 'unknown') - - def thumbnail_picture(self): - fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'mimetype_icons', '%s.png' % (self.mime_type).replace('/', '-')) - if not os.path.exists(fn): - fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'mimetype_icons', 'unknown.png') - return open(fn, 'rb').read() - - def webnail_picture(self): - return self.thumbnail_picture() diff --git a/views/xnail.py b/views/xnail.py new file mode 100644 index 0000000..b6b8cda --- /dev/null +++ b/views/xnail.py @@ -0,0 +1,134 @@ +from django.conf import settings +import fstools +import logging +import mimetypes +import media +from ..models import get_item_type, TYPE_IMAGE, TYPE_VIDEO +import os +import pygal + +# Get a logger instance +logger = logging.getLogger("CACHING") + + +def get_image_class(full_path): + return { + TYPE_IMAGE: image, + TYPE_VIDEO: video, + }.get(get_item_type(full_path), other) + + +def get_image_instance(full_path, request): + return get_image_class(full_path)(full_path, request) + + +class base_item(object): + MIME_TYPES = { + '.ada': 'text/x/adasrc', + '.hex': 'text/x/hex', + '.jpg': 'image/x/generic', + '.jpeg': 'image/x/generic', + '.jpe': 'image/x/generic', + '.png': 'image/x/generic', + '.tif': 'image/x/generic', + '.tiff': 'image/x/generic', + '.gif': 'image/x/generic', + '.avi': 'video/x/generic', + '.mpg': 'video/x/generic', + '.mpeg': 'video/x/generic', + '.mpe': 'video/x/generic', + '.mov': 'video/x/generic', + '.qt': 'video/x/generic', + '.mp4': 'video/x/generic', + '.webm': 'video/x/generic', + '.ogv': 'video/x/generic', + '.flv': 'video/x/generic', + '.3gp': 'video/x/generic', + } + + def __init__(self, full_path, request): + self.full_path = full_path + self.request = request + self.rel_path = pygal.get_rel_path(full_path) + # + ext = os.path.splitext(self.full_path)[1].lower() + self.mime_type = self.MIME_TYPES.get(ext, mimetypes.types_map.get(ext, 'unknown')) + + def __cache_image_folder__(self, rel_path): + return os.path.join(settings.XNAIL_ROOT, rel_path.replace('_', '__').replace('/', '_')) + + def __cache_image_name__(self, max_size): + filename = '%04d_%02x_%s.jpg' % (max_size, self.XNAIL_VERSION_NUMBER, fstools.uid(self.full_path, None)) + return os.path.join(self.__cache_image_folder__(self.rel_path), filename) + + def __delete_cache_image__(self, max_size): + folder = self.__cache_image_folder__(self.rel_path) + if os.path.isdir(folder): + for fn in fstools.filelist(folder, '%04d*' % max_size): + try: + os.remove(fn) + except OSError: + pass # possibly file is already removed by another process + + +class image(base_item): + XNAIL_VERSION_NUMBER = 1 + + def __init__(self, *args, **kwargs): + base_item.__init__(self, *args, **kwargs) + self.mime_type_xnails = mimetypes.types_map.get('.jpg', 'unknown') + + def get_resized_image_data(self, max_size): + cache_filename = self.__cache_image_name__(max_size) + if not os.path.exists(cache_filename): + logger.info('Creating xnail-%d for %s', max_size, self.rel_path) + self.__delete_cache_image__(max_size) + im = media.image(self.full_path) + im.resize(max_size) + im.rotate_by_orientation() + # + # create cache file + # + fstools.mkdir(os.path.dirname(cache_filename)) + im.save(cache_filename) + return im.image_data() + return open(cache_filename, 'rb').read() + + def thumbnail_picture(self): + return self.get_resized_image_data(pygal.get_thumbnail_max_size(self.request)) + + def webnail_picture(self): + return self.get_resized_image_data(pygal.get_webnail_size(self.request)) + + +class video(image): + def get_resized_image_data(self, max_size): + cache_filename = self.__cache_image_name__(max_size) + if not os.path.exists(cache_filename): + logger.info('Creating xnail-%d for %s', max_size, self.rel_path) + self.__delete_cache_image__(max_size) + im = media.image(self.full_path) + im.resize(max_size) + im.join(os.path.join(os.path.dirname(__file__), 'video.png')) + # + # create cache file + # + fstools.mkdir(os.path.dirname(cache_filename)) + im.save(cache_filename) + return im.image_data() + return open(cache_filename, 'rb').read() + + +class other(base_item): + def __init__(self, *args, **kwargs): + base_item.__init__(self, *args, **kwargs) + self.mime_type_xnails = mimetypes.types_map.get('.png', 'unknown') + + def thumbnail_picture(self): + fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'mimetype_icons', '%s.png' % (self.mime_type).replace('/', '-')) + if not os.path.exists(fn): + fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'mimetype_icons', 'unknown.png') + return open(fn, 'rb').read() + + def webnail_picture(self): + return self.thumbnail_picture()