"""
Helper classes and functions for Digikam images.
"""
import logging
import os
from datetime import datetime
from enum import IntEnum, IntFlag
from itertools import groupby
from typing import Any, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
from sqlalchemy import Column, Integer, String, delete, text
from sqlalchemy.orm import relationship, validates
from .table import DigikamTable
from .properties import BasicProperties
from .exceptions import DigikamQueryError, DigikamAssignmentError
from .types import (
ExifExposureMode as ExposureMode,
ExifExposureProgram as ExposureProgram,
ExifFlash as Flash,
ExifMeteringMode as MeteringMode,
ExifOrientation as Orientation,
ExifSubjectDistanceRange as SubjectDistanceRange,
ExifWhiteBalance as WhiteBalance,
ImageColorModel as ColorModel
)
log = logging.getLogger(__name__)
def _imagecopyrightentry_class(dk: 'Digikam') -> type: # noqa: F821
"""Returns the ImageCopyrightEntry class."""
return dk.images.ImageCopyrightEntry
def _imageproperty_class(dk: 'Digikam') -> type: # noqa: F821
"""Returns the TagProperty class."""
return dk.images.ImageProperty
[docs]
class ImageProperties(BasicProperties):
"""
Image Properties
Objects of this type are normally accessed through an :class:`Image`
object, see :attr:`~Image.properties`. In general, it is not necessary
to call the constructor directly.
Individual properties can be retrieved and set dict-like vie the ``[]``
operator. The method :meth:`~ImageProperties.items` iterates over all
properties and yields (key, value) tuples.
Args:
parent: Image object the properties belong to.
"""
# Funktion returning the table class
_class_function = _imageproperty_class
#: Parent id column
_parent_id_col = '_imageid'
#: Key column
_key_col = '_property'
#: Value column
_value_col = '_value'
[docs]
class ImageCopyright(BasicProperties):
"""
Encapsulates ImageCopyright.
Individual copyright entries can be accessed similar to a :class:`dict`.
Values can be:
* A string that is copied to the value column, the extravalue column
will be None in this case.
* A sequence of (value, extravalue) pairs.
If extraValue is set, a tuple is returned, else a str. If no row with a
given key exists, ``None`` is returned.
Args:
parent: The corresponding ``Image`` object.
"""
# Mapped class
_class_function = _imagecopyrightentry_class
#: Parent id column
_parent_id_col = '_imageid'
#: Key column
_key_col = ('_property', '_value', '_extraValue')
#: Value columns
_value_col = ('_value', '_extraValue')
#: Return ``None`` when select does not find a row
_raise_on_not_found = False
#: Remove item when set to ``None``
_remove_on_set_none = True
def __getitem__(
self,
prop: Union[str, int, Iterable]
) -> Union[str, List, Tuple, None]:
ret = [
self._post_process_value(entry)
for entry in self._select_prop(prop)
]
if len(ret) == 0:
return None
if len(ret) == 1:
return ret[0]
return ret
def __setitem__(
self,
prop: Union[str, int, Iterable],
value: Union[str, Tuple, Sequence[Tuple], None]
):
log.debug(
'Setting copyright info %s of image %d (%s)',
prop,
self._parent.id,
self._parent.name
)
self.remove(prop)
if value is None:
log.debug('Setting to None - removing values')
return
log.debug('Setting values:')
if isinstance(value, str):
value = [(value, None)]
elif isinstance(value, tuple):
value = [value]
kwargs = self._key_kwargs(prop)
kwargs.update({self._parent_id_col: self._parent.id})
for v, ev in value:
log.debug('- Setting value %s, %s', v, ev)
kwargs.update(value = v, extraValue = ev)
self._insert(**kwargs)
[docs]
def items(self) -> Iterable:
for prop, rows in groupby(
self._select_self().order_by(text('property')),
lambda x: x._property,
):
value = [self._post_process_value(row) for row in rows]
if len(value) == 0:
value = None
if len(value) == 1:
value = value[0]
if value[1] is None:
value = value[0]
yield prop, value
[docs]
def remove(self, prop: Union[str, int, Iterable]):
"""
Removes the given property.
Args:
prop: Property to remove.
"""
log.debug(
'Removing %s[%s] from %d',
self.__class__.__name__,
prop,
self._parent.id,
)
for row in self._select_prop(prop):
self._session.delete(row)
def _key_kwargs(self, prop: Union[str, int, Iterable]):
"""Prepare kwargs for key"""
kwargs = {}
for k, v in dict(zip(self._key_col, self._pre_process_key(prop))).items():
if v is None:
log.debug('Discarding key %s', k)
else:
kwargs[k] = v
if not '_property' in kwargs:
raise DigikamQueryError('Search term must contain `property`')
return kwargs
def _select_prop(
self,
prop: Union[str, int, Iterable, None]
) -> '~sqlalchemy.orm.Query': # noqa: F821
"""Selects a specific property."""
return self._select_self().filter_by(**self._key_kwargs(prop))
def _post_process_value(
self,
obj: 'DigikamObject' # noqa: F821
) -> Union[str, int, Tuple, None]:
"""
Postprocesses values from [] operations.
"""
val = super()._post_process_value(obj)
# Only return value if extraValue is None:
if val[1] is None:
return val[0]
return val
def define_image_helper_tables(container: 'Images'): # noqa: F821, C901
"""Defines the classes for helper tables."""
dk = container.digikam
if dk.is_mysql:
from sqlalchemy.dialects import mysql
else:
from sqlalchemy.dialects import sqlite
class ImageComment(dk.base):
"""
Digikam Image Comment
Data in this table should be accessed through the ``Image`` properties
:attr:`~Image.caption` and :attr:`~Image.title`.
"""
__tablename__ = 'ImageComments'
if not dk.is_mysql:
_date = Column('date', sqlite.DATETIME(
storage_format = '%(year)04d-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02d', # noqa: E501
regexp = r'(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)'
))
class ImageCopyrightEntry(dk.base):
"""
Digikam Image Copyright
"""
__tablename__ = 'ImageCopyright'
class ImageHistory(dk.base):
"""
Digikam Image History
"""
__tablename__ = 'ImageHistory'
@property
def uuid(self) -> str:
"""ImageHistory.uuid (read-only)"""
return self._uuid
@property
def history(self) -> str:
"""
The image's history data (read-only)
If not None, this field contains XML data describing previous
versions of the image.
"""
return self._history
class ImageInformation(dk.base):
"""
Represents a row of the `ImageInformation` table.
This data comes from the image file and should not be changed, with the
exception of :attr:`~ImageInformation.rating`.
"""
__tablename__ = 'ImageInformation'
@validates('_rating')
def _validate_rating(self, key: int, value: int) -> int:
if value < -1 or value > 5:
raise DigikamAssignmentError('Image rating must be from -1 to 5')
return value
if not dk.is_mysql:
_creationDate = Column(
'creationDate',
sqlite.DATETIME(
storage_format = '%(year)04d-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02d', # noqa: E501
regexp = r'(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)'
)
)
_digitizationDate = Column(
'digitizationDate',
sqlite.DATETIME(
storage_format = '%(year)04d-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02d', # noqa: E501
regexp = r'(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)'
)
)
@property
def rating(self) -> int:
"""The image's rating (from -1 to 5)"""
return self._rating
@rating.setter
def rating(self, value: int):
self._rating = value
@property
def creationDate(self) -> datetime:
"""The image's creation date (read-only)"""
return self._creationDate
@property
def digitizationDate(self) -> datetime:
"""The image's digitization date (read-only)"""
return self._digitizationDate
@property
def orientation(self) -> Optional[Orientation]:
"""The image's orientation (read-only)"""
if self._orientation:
return Orientation(self._orientation)
else:
return None
@property
def width(self) -> int:
"""The image's width (read-only)"""
return self._width
@property
def height(self) -> int:
"""The image's height (read-only)"""
return self._height
@property
def format(self) -> str:
"""The image's file format (read-only)"""
return self._format
@property
def colorDepth(self) -> int:
"""The image's color depth (read-only)"""
return self._colorDepth
@property
def colorModel(self) -> Union[ColorModel,int,None]:
"""
The image's color model (read-only)
.. versionchanged:: 0.3.2
Return numeric value if it is not a valid :class:`ExifColorModel`
"""
if self._colorModel is None:
return None
else:
try:
return ColorModel(self._colorModel)
except ValueError:
log.warning('%d is not a valid color model', self._colorModel)
return self._colorModel
@validates('_orientation', '_colorModel')
def _convert_to_int(self, key, value):
return int(value)
class ImageMetadata(dk.base):
"""
Represents a row of the ``ImageMetadata`` table.
This object contains Exif information of the corresponding image.
"""
__tablename__ = 'ImageMetadata'
# Retrieve double as float
if dk.is_mysql:
_aperture = Column('aperture', mysql.DOUBLE(asdecimal = False))
_focalLength = Column('focalLength', mysql.DOUBLE(asdecimal = False))
_focalLength35 = Column('focalLength35', mysql.DOUBLE(asdecimal = False))
_exposureTime = Column('exposureTime', mysql.DOUBLE(asdecimal = False))
_subjectDistance = Column('subjectDistance', mysql.DOUBLE(asdecimal = False))
@property
def make(self) -> Optional[str]:
"""Camera's manufacturer"""
return self._make
@property
def model(self) -> Optional[str]:
"""Camera model"""
return self._model
@property
def lens(self) -> Optional[str]:
"""Lens model"""
return self._lens
@property
def aperture(self) -> Optional[float]:
"""The image's aperture value/f-number (read-only)"""
return self._aperture
@property
def focalLength(self) -> Optional[float]:
"""The image's focal length (read-only)"""
return self._focalLength
@property
def focalLength35(self) -> Optional[float]:
"""The image's 35mm-equivalent focal length (read-only)"""
return self._focalLength35
@property
def exposureTime(self) -> Optional[float]:
"""The image's exposure time (read-only)"""
return self._exposureTime
@property
def exposureProgram(self) -> Optional[ExposureProgram]:
"""The image's exposure program (read-only)"""
if self._exposureProgram is None:
return None
return ExposureProgram(self._exposureProgram)
@property
def exposureMode(self) -> Optional[ExposureMode]:
"""The image's exposure mode (read-only)"""
if self._exposureMode is None:
return None
return ExposureMode(self._exposureMode)
@property
def sensitivity(self) -> Optional[int]:
"""Photographic sensitivity (ISO number)"""
return self._sensitivity
@property
def flash(self) -> Optional[Flash]:
"""Information about flash usage"""
if self._flash is None:
return None
return Flash(self._flash)
@property
def whiteBalance(self) -> Optional[WhiteBalance]:
"""White balance mode"""
if self._whiteBalance is None:
return None
return WhiteBalance(self._whiteBalance)
@property
def whiteBalanceColorTemperature(self) -> Optional[int]:
"""Color temperature for manual white balance"""
return self._whiteBalanceColorTemperature
@property
def meteringMode(self) -> Optional[MeteringMode]:
"""The image's metering mode"""
if self._meteringMode is None:
return None
return MeteringMode(self._meteringMode)
@property
def subjectDistance(self) -> Optional[float]:
"""Distance to subject, as measured by the camera"""
return self._subjectDistance
@property
def subjectDistanceCategory(self) -> Optional[SubjectDistanceRange]:
"""Exif SubjectDistanceRange attribute"""
if self._subjectDistanceCategory is None:
return None
return SubjectDistanceRange(self._subjectDistanceCategory)
@validates(
'_exposureMode', '_exposureProgram',
'_flash',
'_meteringMode',
'_subjectDistanceCategory',
'_whiteBalance'
)
def _convert_to_int(self, key: str, value: int):
return int(value)
class ImagePosition(dk.base):
"""
Contains the Image's position.
Should be accessed through :attr:`Image.position`.
"""
__tablename__ = 'ImagePositions'
# Retrieve double as float
if dk.is_mysql:
_latitudeNumber = Column(
'latitudeNumber',
mysql.DOUBLE(asdecimal = False)
)
_longitudeNumber = Column(
'longitudeNumber',
mysql.DOUBLE(asdecimal = False)
)
_altitude = Column(
'altitude',
mysql.DOUBLE(asdecimal = False)
)
_orientation = Column(
'orientation',
mysql.DOUBLE(asdecimal = False)
)
_tilt = Column(
'tilt',
mysql.DOUBLE(asdecimal = False)
)
_roll = Column(
'roll',
mysql.DOUBLE(asdecimal = False)
)
_accuracy = Column(
'accuracy',
mysql.DOUBLE(asdecimal = False)
)
class ImageProperty(dk.base):
"""
Image Properties
This table should be accessed via
Class :class:`~digikamdb.images.ImageProperties`.
"""
__tablename__ = 'ImageProperties'
_imageid = Column('imageid', Integer, primary_key = True)
_property = Column('property', String, primary_key = True)
class VideoMetadata(dk.base):
"""
Digikam Video Metadata
This object contains Video metadata of the corresponding
:class:`Image` object.
"""
__tablename__ = 'VideoMetadata'
@property
def aspectRatio(self) -> str:
return self._aspectRatio
@property
def audioBitRate(self) -> str:
return self._audioBitRate
@property
def audioChannelType(self) -> str:
return self._audioChannelType
@property
def audioCompressor(self) -> str:
return self._audioCompressor
@property
def duration(self) -> str:
return self._duration
@property
def frameRate(self) -> str:
return self._frameRate
@property
def exposureProgram(self) -> int:
return self._exposureProgram
@property
def videoCodec(self) -> str:
return self._videoCodec
# Set the properties:
container.ImageComment = ImageComment
container.ImageCopyrightEntry = ImageCopyrightEntry
container.ImageHistory = ImageHistory
container.ImageInformation = ImageInformation
container.ImageMetadata = ImageMetadata
container.ImagePosition = ImagePosition
container.ImageProperty = ImageProperty
container.VideoMetadata = VideoMetadata