222 lines
6.9 KiB
Python
222 lines
6.9 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
"""
|
|
media (Media Tools)
|
|
===================
|
|
|
|
**Author:**
|
|
|
|
* Dirk Alders <sudo-dirk@mount-mockery.de>
|
|
|
|
**Description:**
|
|
|
|
This module helps on all issues with media files, like tags (e.g. exif, id3) and transformations.
|
|
|
|
**Submodules:**
|
|
|
|
* :func:`media.get_media_data`
|
|
* :class:`media.image`
|
|
|
|
**Unittest:**
|
|
|
|
See also the :download:`unittest <../../media/_testresults_/unittest.pdf>` documentation.
|
|
"""
|
|
__DEPENDENCIES__ = []
|
|
|
|
import io
|
|
import logging
|
|
from PIL import Image, ImageEnhance, ExifTags
|
|
|
|
logger_name = 'MEDIA'
|
|
logger = logging.getLogger(logger_name)
|
|
|
|
|
|
__DESCRIPTION__ = """The Module {\\tt %s} is designed to help on all issues with media files, like tags (e.g. exif, id3) and transformations.
|
|
For more Information read the documentation.""" % __name__.replace('_', '\_')
|
|
"""The Module Description"""
|
|
__INTERPRETER__ = (3, )
|
|
"""The Tested Interpreter-Versions"""
|
|
|
|
|
|
KEY_ALBUM = 'album'
|
|
KEY_APERTURE = 'aperture'
|
|
KEY_ARTIST = 'artist'
|
|
KEY_BITRATE = 'bitrate'
|
|
KEY_CAMERA = 'camera'
|
|
KEY_DURATION = 'duration'
|
|
KEY_EXPOSURE_PROGRAM = 'exposure_program'
|
|
KEY_EXPOSURE_TIME = 'exposure_time'
|
|
KEY_FLASH = 'flash'
|
|
KEY_FOCAL_LENGTH = 'focal_length'
|
|
KEY_GENRE = 'genre'
|
|
KEY_GPS = 'gps'
|
|
KEY_HEIGHT = 'height'
|
|
KEY_ISO = 'iso'
|
|
KEY_ORIENTATION = 'orientation'
|
|
KEY_RATIO = 'ratio'
|
|
KEY_SIZE = 'size'
|
|
KEY_TIME = 'time' # USE time.localtime(value) or datetime.fromtimestamp(value) to convert the timestamp
|
|
KEY_TIME_IS_SUBSTITUTION = 'tm_is_subst'
|
|
KEY_TITLE = 'title'
|
|
KEY_TRACK = 'track'
|
|
KEY_WIDTH = 'width'
|
|
KEY_YEAR = 'year'
|
|
|
|
|
|
def get_media_data(full_path):
|
|
from media.metadata import get_audio_data, get_image_data, get_video_data
|
|
from media.common import get_filetype, FILETYPE_AUDIO, FILETYPE_IMAGE, FILETYPE_VIDEO
|
|
#
|
|
ft = get_filetype(full_path)
|
|
#
|
|
if ft == FILETYPE_AUDIO:
|
|
return get_audio_data(full_path)
|
|
elif ft == FILETYPE_IMAGE:
|
|
return get_image_data(full_path)
|
|
elif ft == FILETYPE_VIDEO:
|
|
return get_video_data(full_path)
|
|
else:
|
|
logger.warning('Filetype not known: %s', full_path)
|
|
|
|
|
|
ORIENTATION_NORMAL = 1
|
|
ORIENTATION_VERTICAL_MIRRORED = 2
|
|
ORIENTATION_HALF_ROTATED = 3
|
|
ORIENTATION_HORIZONTAL_MIRRORED = 4
|
|
ORIENTATION_LEFT_ROTATED = 6
|
|
ORIENTATION_RIGHT_ROTATED = 8
|
|
|
|
JOIN_TOP_LEFT = 1
|
|
JOIN_TOP_RIGHT = 2
|
|
JOIN_BOT_LEFT = 3
|
|
JOIN_BOT_RIGHT = 4
|
|
JOIN_CENTER = 5
|
|
|
|
|
|
class image(object):
|
|
def __init__(self, media_instance=None):
|
|
if media_instance is not None:
|
|
self.load_from_file(media_instance)
|
|
else:
|
|
self._im = None
|
|
|
|
def load_from_file(self, media_instance):
|
|
from media.convert import get_pil_image
|
|
#
|
|
self._im = get_pil_image(media_instance)
|
|
if self._im is None:
|
|
return False
|
|
try:
|
|
self._exif = dict(self._im._getexif().items())
|
|
except AttributeError:
|
|
self._exif = {}
|
|
if type(self._im) is not Image.Image:
|
|
self._im = self._im.copy()
|
|
logger.debug('loading image from %s', repr(media_instance))
|
|
return True
|
|
|
|
def save(self, full_path):
|
|
if self._im is None:
|
|
logger.warning('No image available to be saved (%s)', repr(full_path))
|
|
return False
|
|
else:
|
|
logger.debug('Saving image to %s', repr(full_path))
|
|
with open(full_path, 'w') as fh:
|
|
im = self._im.convert('RGB')
|
|
im.save(fh, 'JPEG')
|
|
return True
|
|
|
|
def image_data(self):
|
|
im = self._im.copy().convert('RGB')
|
|
output = io.BytesIO()
|
|
im.save(output, format='JPEG')
|
|
return output.getvalue()
|
|
|
|
def resize(self, max_size):
|
|
if self._im is None:
|
|
logger.warning('No image available to be resized')
|
|
return False
|
|
else:
|
|
logger.debug('Resizing picture to max %d pixel in whatever direction', max_size)
|
|
x, y = self._im.size
|
|
xy_max = max(x, y)
|
|
self._im = self._im.resize((int(x * float(max_size) / xy_max), int(y * float(max_size) / xy_max)), Image.NEAREST).rotate(0)
|
|
return True
|
|
|
|
def rotate_by_orientation(self, orientation=None):
|
|
if self._im is None:
|
|
logger.warning('No image available, rotation not possible')
|
|
return False
|
|
|
|
if orientation is None:
|
|
exif_tags = dict((v, k) for k, v in ExifTags.TAGS.items())
|
|
try:
|
|
orientation = self._exif[exif_tags['Orientation']]
|
|
logger.debug("No orientation given, orientation %s extract from exif data", repr(orientation))
|
|
except KeyError:
|
|
return False
|
|
|
|
if orientation == ORIENTATION_HALF_ROTATED:
|
|
angle = 180
|
|
elif orientation == ORIENTATION_LEFT_ROTATED:
|
|
angle = 270
|
|
elif orientation == ORIENTATION_RIGHT_ROTATED:
|
|
angle = 90
|
|
else:
|
|
if type(orientation) == int and orientation > 8:
|
|
logger.warning('Orientation %s unknown for rotation', repr(orientation))
|
|
return False
|
|
logger.debug('Rotating picture by %d°', angle)
|
|
self._im = self._im.rotate(angle, expand=True)
|
|
return True
|
|
|
|
def join(self, join_image, join_pos=JOIN_TOP_RIGHT, opacity=0.7):
|
|
from media.convert import get_pil_image
|
|
|
|
def rgba_copy(im):
|
|
if im.mode != 'RGBA':
|
|
return im.convert('RGBA')
|
|
else:
|
|
return im.copy()
|
|
|
|
if self._im is None:
|
|
logger.warning('No image available, joining not possible')
|
|
return False
|
|
|
|
# ensure type of join_image is PIL.Image
|
|
join_image = get_pil_image(join_image)
|
|
if join_image is None:
|
|
logger.warning('Image to be joined is not supported %s', repr(join_image))
|
|
return False
|
|
|
|
im2 = rgba_copy(join_image)
|
|
# change opacity of im2
|
|
alpha = im2.split()[3]
|
|
alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
|
|
im2.putalpha(alpha)
|
|
|
|
self._im = rgba_copy(self._im)
|
|
|
|
# create a transparent layer
|
|
layer = Image.new('RGBA', self._im.size, (0, 0, 0, 0))
|
|
# draw im2 in layer
|
|
if join_pos == JOIN_TOP_LEFT:
|
|
layer.paste(im2, (0, 0))
|
|
elif join_pos == JOIN_TOP_RIGHT:
|
|
layer.paste(im2, ((self._im.size[0] - im2.size[0]), 0))
|
|
elif join_pos == JOIN_BOT_LEFT:
|
|
layer.paste(im2, (0, (self._im.size[1] - im2.size[1])))
|
|
elif join_pos == JOIN_BOT_RIGHT:
|
|
layer.paste(im2, ((self._im.size[0] - im2.size[0]), (self._im.size[1] - im2.size[1])))
|
|
elif join_pos == JOIN_CENTER:
|
|
layer.paste(im2, (int((self._im.size[0] - im2.size[0]) / 2), int((self._im.size[1] - im2.size[1]) / 2)))
|
|
else:
|
|
logger.warning("Join position value %s is not supported", join_pos)
|
|
return False
|
|
|
|
logger.debug('Joining two images')
|
|
self._im = Image.composite(layer, self._im, layer)
|
|
|
|
return True
|