Source code for digikamdb.image_helpers

"""
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