"""Tags management
Tags are keywords which can be attached to images.
Tags can be retrieved (get) and modified (add, set, remove).
Other actions may be available.
Part of the tagit module.
A copy of the license is provided with the project.
Author: Matthias Baumgartner, 2016
"""
# STANDARD IMPORTS
import itertools
# INNER-MODULE IMPORTS
from ..basics import fst, unique, union
# EXPORTS
__all__ = ('Tags_SQLite', )
## CODE ##
[docs]class Tags(object):
"""The Tags baseclass and interface.
Tags are keywords which can be attached to images.
Tags can be retrieved (get) and modified (add, set, remove).
Other actions may be available.
"""
[docs] def query(self, tags):
"""Return all images which satisfy all constraining *tags*."""
abstract()
[docs]class Tags_SQLite(object):
"""Tag operations based on a SQLite database.
The database link is to be passed as *conn*.
Database changes are not saved.
It's assumed that the basic scheme is present (i.e. image table).
"""
def __init__(self, conn):
self.conn = conn
self.query_sort = "image.t_image_create ASC" # FIXME: Not really a feature yet
#self.query_sort = "image.id ASC"
#self.query_sort = "image.t_db_update ASC"
#self.query_sort = "image.path ASC"
###########################################################################
#### GETTERS ####
###########################################################################
def get_all(self):
return map(fst, self.conn.execute("SELECT tag FROM tag").fetchall())
def get(self, image):
results = self.conn.execute("SELECT tag.tag FROM tag JOIN img_tag ON img_tag.tag = tag.id JOIN image ON img_tag.image = image.id WHERE image.path = ?", (image, ))
return map(fst, results.fetchall())
[docs] def query(self, tags):
"""Return all images which satisfy all constraining *tags*.
.. TODO::
There's a SQLite limit for joins, usually 64. Meaning we're limited to 31 search tags.
This seems ok for the moment but one might want to consider ways out of this.
"""
joins, conds, binds = [], [], []
for i in range(len(tags)):
joins.append("img_tag AS img_tag_{0} ON img_tag_{0}.image = image.id".format(i))
joins.append("tag AS tag_{0} ON img_tag_{0}.tag = tag_{0}.id".format(i))
conds.append("tag_{0}.tag LIKE ?".format(i)) # LIKE operator is case insensitive
binds.append(tags[i].lower())
return joins, conds, binds
###########################################################################
#### TAGS METRICS ####
###########################################################################
def histogram(self, include=None, exclude=None):
if include is None: include = []
if exclude is None: exclude = []
query = "SELECT tag.tag, count(img_tag.tag) FROM img_tag"
query += " JOIN tag ON tag.id = img_tag.tag"
if len(include) + len(exclude) > 0:
query += " JOIN image ON image.id = img_tag.image"
query += " WHERE "
if len(include) > 0:
query += ' image.path IN (' + ','.join('?' * len(include)) + ')'
if len(exclude) > 0:
query += len(include) > 0 and ' AND ' or ''
query += ' image.path NOT IN (' + ','.join('?' * len(exclude)) + ')'
query += ' GROUP BY img_tag.tag'
return dict(self.conn.execute(query, include + exclude).fetchall())
###########################################################################
#### TAGS OPERATIONS ####
###########################################################################
def remove(self, images, tags):
if len(tags) == 0 or len(images) == 0: return
# Remove references
args = itertools.product(tags, images)
self.conn.executemany("DELETE FROM img_tag WHERE tag = (SELECT id FROM tag WHERE tag = ?) AND image = (SELECT id FROM image WHERE path = ?)", args)
# Cleanup
for tag in tags:
self._remove_orphan_tag(tag)
def set(self, images, tags):
for img in images:
# Create the diff
current = self.get(img)
added = filter(lambda itm: itm not in current, tags)
removed = filter(lambda itm: itm not in tags, current)
# Add/remove acc. to diff
self.add([img], added)
self.remove([img], removed)
[docs] def add(self, images, tags):
"""Add all *tags* to each of *images*.
*images* is a list of images (path). If an image is not in the database, it is ignored.
*tags* is a list of strings. If a tag is not in the database, it is added.
"""
if len(images) == 0 or len(tags) == 0: return
# Get ids of *images*
img_ids = [self.conn.execute("SELECT id FROM image WHERE path = ?", (img, )).fetchone() for img in images]
img_ids = filter(lambda id_: id_ is not None, img_ids)
img_ids = map(fst, img_ids)
if len(img_ids) == 0:
# No valid images given
# NOTE: Error management
return
# Add *tags* to database
# FIXME: Find better solution (e.g. use INSERT OR ....)
tag_ids = []
for keyword in tags:
result = self.conn.execute("SELECT id FROM tag WHERE tag = ?", (keyword, )).fetchone()
if result is None:
tag_ids.append(self.conn.execute("INSERT INTO tag (tag) VALUES (?)", (keyword, )).lastrowid)
else:
tag_ids.append(result[0])
# Attach tags to images
references = itertools.product(img_ids, tag_ids)
self.conn.executemany("INSERT OR IGNORE INTO img_tag (image, tag) VALUES (?, ?)", references)
def remove_all(self, images):
tags = []
for img in images:
tags += self.conn.execute('SELECT tag.tag FROM tag JOIN img_tag on img_tag.tag = tag.id JOIN image on img_tag.image = image.id WHERE image.path = ?', (img, )).fetchall()
self.conn.execute('DELETE FROM img_tag WHERE image = (SELECT id FROM image WHERE path = ?)', (img, ))
# Cleanup
tags = unique(map(fst, tags))
for tag in tags:
self._remove_orphan_tag(tag)
[docs] def cleanup(self):
"""Search and remove tags without images.
This operation may take some time.
"""
orphans = self.conn.execute("SELECT id FROM tag WHERE id NOT IN (SELECT tag FROM img_tag GROUP BY tag)").fetchall()
orphans = map(fst, orphans)
for t_id in orphans:
self.conn.execute("DELETE FROM tag WHERE id = ?", (t_id, ))
[docs] def rename(self, source, target):
"""Rename a tag from *source* to *target*.
*source* and *target* are the tag names.
If *target* exists, the tags will be merged.
"""
# Check source
q_src = self.conn.execute("SELECT id FROM tag WHERE tag = ?", (source, )).fetchall()
if len(q_src) == 0: return # Source doesn't exist, no further action required
src_id = fst(q_src[0])
# Check target
q_trg = self.conn.execute("SELECT id FROM tag WHERE tag = ?", (target, )).fetchall()
if len(q_trg) == 0: # Target doesn't exist, rename
self.conn.execute("UPDATE tag SET tag = ? WHERE id = ?", (target, src_id))
else: # Target does exist, move
trg_id = fst(q_trg[0])
self.conn.execute("UPDATE OR REPLACE img_tag SET tag = ? WHERE tag = ?", (trg_id, src_id))
self.conn.execute("DELETE FROM tag WHERE id = ?", (src_id, ))
###########################################################################
#### INTERNALS ####
###########################################################################
def _remove_orphan_tag(self, tag):
"""Remove a tag if no image refers to it."""
num_references = self.conn.execute("SELECT COUNT(*) FROM img_tag JOIN tag ON img_tag.tag = tag.id WHERE tag.tag = ?", (tag, )).fetchone()
if fst(num_references) == 0:
self.conn.execute("DELETE FROM tag WHERE id = (SELECT id FROM tag WHERE tag = ?)", (tag, ))
[docs]class Tags_Exif(Tags):
"""Tags-like interface to EXIF/IPTC metadata.
A *meta_adapter* is required to do the IPTC operations.
Some operations are not meaningful and return dummy values.
"""
def __init__(self, adapter):
self.meta_adapter = adapter
def get_all(self):
return []
def get(self, image):
return self.meta_adapter.get(image)
def query(self, tags):
return []
def remove(self, images, tags):
for img in images:
current = self.meta_adapter.get(img)
new = filter(lambda t: t not in tags, current)
self.meta_adapter.set(img, new)
def set(self, images, tags):
for img in images:
self.meta_adapter.set(img, tags)
def add(self, images, tags):
for img in images:
self.add(img, tags)
def remove_all(self, images):
for img in images:
self.meta_adapter.set(img, [])
def cleanup(self):
pass
## EOF ##