"""
Provides access to Digikam images.
Digikam images can be accessed via the ``Digikam`` property
:attr:`~Digikam.images`, which is an object of class :class:`Images`.
"""
import logging
import os
from datetime import datetime
from itertools import groupby
from typing import Iterable, List, 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 .image_comments import ImageCaptions, ImageTitles
from .image_helpers import ImageCopyright, ImageProperties, define_image_helper_tables
from .types import ImageCategory as Category, ImageStatus as Status
from .exceptions import DigikamVersionError
log = logging.getLogger(__name__)
def _image_class(dk: 'Digikam') -> type: # noqa: F821, C901
"""
Returns the Image class.
"""
if not dk.is_mysql:
from sqlalchemy.dialects import sqlite
class Image(dk.base):
"""
Represents a row in the table ``Images``.
The image's album can be accessed by :attr:`album`.
Digikam splits metadata (Exif and own) in several tables. `Image`
has the corresponding properties:
* :attr:`captions` and :attr:`titles`
* :attr:`copyright`
* :attr:`imagemeta` (for pictures)
* :attr:`information`
* :attr:`position`
* :attr:`properties`
* :attr:`tags`
* :attr:`videometa` (for movies)
See also:
* Class :class:`~digikamdb.images.Images`
"""
__tablename__ = 'Images'
if not dk.is_mysql:
_modificationDate = Column(
'modificationDate',
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+)')
)
_albumObj = relationship(
'Album',
primaryjoin = 'foreign(Image._album) == Album._id',
back_populates = '_images'
)
_comments = relationship(
'ImageComment',
primaryjoin = 'foreign(ImageComment._imageid) == Image._id',
lazy = 'dynamic'
)
_copyright = relationship(
'ImageCopyrightEntry',
primaryjoin = 'foreign(ImageCopyrightEntry._imageid) == Image._id',
lazy = 'dynamic'
)
_history = relationship(
'ImageHistory',
primaryjoin = 'foreign(ImageHistory._imageid) == Image._id',
uselist = False
)
_information = relationship(
'ImageInformation',
primaryjoin = 'foreign(ImageInformation._imageid) == Image._id',
uselist = False
)
_metadata = relationship(
'ImageMetadata',
primaryjoin = 'foreign(ImageMetadata._imageid) == Image._id',
uselist = False
)
_position = relationship(
'ImagePosition',
primaryjoin = 'foreign(ImagePosition._imageid) == Image._id',
uselist = False
)
_properties = relationship(
'ImageProperty',
primaryjoin = 'foreign(ImageProperty._imageid) == Image._id',
lazy = 'dynamic'
)
_tags = relationship(
'Tag',
primaryjoin = 'foreign(ImageTags.c.imageid) == Image._id',
secondaryjoin = 'foreign(ImageTags.c.tagid) == Tag._id',
secondary = 'ImageTags',
back_populates = '_images'
)
_videometadata = relationship(
'VideoMetadata',
primaryjoin = 'foreign(VideoMetadata._imageid) == Image._id',
uselist = False
)
#: Needed for access to related tables
_session = dk.session
# -- Relationship properties -----------------------------------------
# Relationship to Albums
@property
def album(self) -> 'Album': # noqa: F821
"""
The album object to which the image belongs (read-only)
This corresponds to the directory the image file where the image
file resides.
"""
return self._albumObj
# Relationship to ImageComments
@property
def captions(self) -> ImageCaptions:
"""
The image's captions object
This property contains all the image's captions. To access the
default caption, you can also use the :attr:`caption` property.
See :class:`Captions` for a more detailed description.
"""
if not hasattr(self, '_captionsObj'):
self._captionsObj = ImageCaptions(self)
return self._captionsObj
@property
def caption(self) -> Optional[str]:
"""
The image's default caption
The default caption has 'x-default' as language and ``None``
as author. For comments in other languages or from other authors,
use :attr:`~Image.captions`.
.. versionchanged:: 0.2.0
Returns a string instead of a tuple.
"""
cap = self.captions['x-default']
if cap is None:
return None
else:
return cap[0]
@caption.setter
def caption(self, val: Optional[str]):
self.captions[''] = val
@property
def titles(self) -> ImageTitles:
"""
The image's titles (no setter)
Digikam supports multilingual titles. To access the title in a
specific language, use the ``[]`` operator:
.. code-block:: python
french = img.titles['fr-FR']
default = img.titles['x-default'] # default language
``titles['']`` or ``titles[None]`` will return the default
language (**x-default**)
"""
if not hasattr(self, '_titlesObj'):
self._titlesObj = ImageTitles(self)
return self._titlesObj
@property
def title(self) -> str:
"""
The image's title in default language ('x-default')
For comments in other languages, use :attr:`~Image.titles`.
"""
return self.titles['']
@title.setter
def title(self, val: Optional[str]):
self.titles[''] = val
# Relationship to ImageCopyright
@property
def copyright(self) -> ImageCopyright:
"""
The image's copyright data (no setter)
"""
if not hasattr(self, '_copyrightObj'):
self._copyrightObj = ImageCopyright(self)
return self._copyrightObj
# Relationship to ImageHistory
@property
def history(self) -> 'ImageHistory': # noqa: F821
"""
The image's history (no setter)
"""
return self._history
# Relationship to ImageInformation
@property
def information(self) -> 'ImageInformation': # noqa: F821
"""
Part of the image's metadata (no setter)
"""
return self._information
# Relationship to ImageMetadata
@property
def imagemeta(self) -> 'ImageMetadata': # noqa: F821
"""
The image's photographic metadata (no setter)
"""
return self._metadata
# Relationship to ImagePositions
@property
def position(self) -> Optional[Tuple]:
"""
The image's GPS location data
The value is a tuple with latitude, longitude and altitude. When
setting the property, latitude and longitude can be given as a
signed float, as a stringified float or as a string containing
the absolute value followed by ``N``, ``S``, ``W`` or ``E``. The
altitude can be omitted, in this case it is not changed if already
present. To remove an existing altitude, give the position as
``(latitude, longitude, None)``
When ``position`` is set to ``None``, the row in ImagePositions
will be deleted.
"""
if self._position:
return (
self._position._latitudeNumber,
self._position._longitudeNumber,
self._position._altitude)
return None
@position.setter
def position(self, pos: Optional[Tuple]):
log.debug(
'Setting position of image %d (%s) to %s',
self.id,
self.name,
pos
)
if pos is None:
if self._position:
self._session.execute(
delete(self.digikam.images.ImagePosition)
.filter_by(_imageid = self.id))
return
lat = pos[0]
lng = pos[1]
if isinstance(lat, str):
if lat[-1] == 'N':
lat = float(lat[:-1])
elif lat[-1] == 'S':
lat = - float(lat[:-1])
else:
lat = float(lat)
if isinstance(lng, str):
if lng[-1] == 'E':
lng = float(lng[:-1])
elif lng[-1] == 'W':
lng = - float(lng[:-1])
else:
lng = float(lng)
if lat < 0:
lat2 = -lat
latstr = '%d,%.8fS' % (int(lat2), (lat2-int(lat2))*60)
else:
latstr = '%d,%.8fN' % (int(lat), (lat-int(lat))*60)
if lng < 0:
lng2 = -lng
lngstr = '%d,%.8fW' % (int(lng2), (lng2-int(lng2))*60)
else:
lngstr = '%d,%.8fE' % (int(lng), (lng-int(lng))*60)
if self._position:
self._position._latitude = latstr
self._position._longitude = lngstr
self._position._latitudeNumber = lat
self._position._longitudeNumber = lng
if len(pos) > 2:
self._position._altitude = pos[2]
else:
alt = None
if len(pos) > 2:
alt = pos[2]
newpos = self.digikam.images.ImagePosition(
_imageid = self.id,
_latitude = latstr,
_longitude = lngstr,
_latitudeNumber = lat,
_longitudeNumber = lng,
_altitude = alt)
self._session.add(newpos)
# self._session.commit()
# Relationship to ImageProperties
@property
def properties(self) -> ImageProperties:
"""
The image's properties (no setter)
See :class:`~_sqla.ImageProperties` for more information.
"""
if not hasattr(self, '_propertiesObj'):
self._propertiesObj = ImageProperties(self)
return self._propertiesObj
# Relationship to Tags
@property
def tags(self) -> Iterable['Tag']: # noqa: F821
"""
The image's tags (no setter)
Tags can be changed by modifying the list.
"""
return self._tags
# Relationship to VideoMetadata
@property
def videometa(self) -> 'VideoMetadata': # noqa: F821
"""Metadata for video files (no setter)"""
return self._videometadata
# Column properties:
@property
def id(self) -> int:
"""The image's id (read-only)"""
return self._id
@property
def name(self) -> str:
"""The image's file name (read-only)"""
return self._name
@property
def status(self) -> Status:
"""
The image's status
The status can be undefined, visible, hidden, trashed or obsolete.
"""
return Status(self._status)
@status.setter
def status(self, value):
self._status = value
@property
def category(self) -> Category:
"""The image's category (read-only)"""
return Category(self._category)
@validates('_status', '_category')
def _convert_to_int(self, key, value):
return int(value)
@property
def modificationDate(self) -> datetime:
"""The image file's modification date (read-only)"""
return self._modificationDate
@property
def fileSize(self) -> int:
"""The image file's size (read-only)"""
return self._fileSize
@property
def uniqueHash(self) -> str:
"""The image's unique (MD5) hash (read-only)"""
return self._uniqueHash
@property
def manualOrder(self) -> int:
"""
The image's manual order in its album (read-only)
Raises:
DigikamVersionError: If DBVersion < 10
.. versionchanged:: 0.2.2
Raises :exc:`DigikamVersionError` for low DB versions.
"""
if self.digikam.db_version < 10:
raise DigikamVersionError(
'manualOrder is present in DBVersion >= 10'
)
return self._manualOrder
# Other properties and members
@property
def abspath(self) -> Optional[str]:
"""
The absolute path of the image file (read-only)
.. versionchanged:: 0.3.5
* Converted to lowercase for case-insensitive roots (except mountpoint).
* Returns `None` if image has no album (e.g. for deleted images).
"""
if self._album is None:
return None
else:
return os.path.join(
self.album.abspath,
self.name if self.album.root.caseSensitivity else self.name.lower()
)
return Image
[docs]
class Images(DigikamTable):
"""
Offers access to the images in the Digikam instance.
``Images`` represents all images present in the Digikam database. It is
usually accessed through the :class:`~digikamdb.connection.Digikam`
property :attr:`~digikamdb.connection.Digikam.images`.
Usage:
.. code-block:: python
dk = Digikam(...)
myimage = dk.images.find('/path/to/my/image.jpg')[0] # by name
myimage = dk.images[42] # by id
for img in dk.images: # iterate
print(img.name)
Parameters:
digikam: Digikam object for access to database and other classes.
See also:
* Class :class:`~_sqla.Image`
"""
_class_function = _image_class
def __init__(
self,
digikam: 'Digikam', # noqa: F821
):
super().__init__(digikam)
define_image_helper_tables(self)
[docs]
def find(
self,
path: Union[str, bytes, os.PathLike]
) -> List['Image']: # noqa: F821
"""
Finds an Image by name.
Args:
path: Path to image file or album directory. Can be given as any
type that the :mod:`os.path` functions understand.
.. todo::
* Enable wildcards
"""
log.debug(
'Images: searching for %s',
path,
)
abspath = os.path.abspath(path)
albums = self.digikam.albums.find(abspath)
if albums:
ret = []
for al in albums:
log.debug('Adding images from album %s to result', al.abspath)
ret.extend(al.images.all())
return ret
# There are no albums on path, so path must
# be an image file if it exists.
base = os.path.basename(abspath)
dir_ = os.path.dirname(abspath)
album = self.digikam.albums.find(dir_, True)
if album:
log.debug('Returning images from album %s', album.abspath)
return album.images.filter_by(_name = base).all()
log.debug('No files found')
return []