You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
digikam/digikam/digikam/metadatahub.cpp

859 lines
26 KiB

/* ============================================================
*
* This file is a part of digiKam project
* http://www.digikam.org
*
* Date : 2007-01-05
* Description : Metadata handling
*
* Copyright (C) 2007-2009 by Marcel Wiesweg <marcel.wiesweg@gmx.de>
* Copyright (C) 2007-2009 by Gilles Caulier <caulier dot gilles at gmail dot com>
*
* This program is free software you can redistribute it
* and/or modify it under the terms of the GNU General
* Public License as published by the Free Software Foundation
* either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* ============================================================ */
// TQt includes.
#include <tqfileinfo.h>
// KDE includes.
// Local includes.
#include "ddebug.h"
#include "imageinfo.h"
#include "album.h"
#include "albummanager.h"
#include "albumsettings.h"
#include "imageattributeswatch.h"
#include "metadatahub.h"
namespace Digikam
{
class MetadataHubPriv
{
public:
MetadataHubPriv()
{
dateTimeStatus = MetadataHub::MetadataInvalid;
ratingStatus = MetadataHub::MetadataInvalid;
commentStatus = MetadataHub::MetadataInvalid;
rating = -1;
highestRating = -1;
count = 0;
dbmode = MetadataHub::ManagedTags;
dateTimeChanged = false;
commentChanged = false;
ratingChanged = false;
tagsChanged = false;
}
MetadataHub::Status dateTimeStatus;
MetadataHub::Status commentStatus;
MetadataHub::Status ratingStatus;
TQDateTime dateTime;
TQDateTime lastDateTime;
TQString comment;
int rating;
int highestRating;
int count;
TQMap<TAlbum *, MetadataHub::TagStatus> tags;
TQStringList tagList;
MetadataHub::DatabaseMode dbmode;
bool dateTimeChanged;
bool commentChanged;
bool ratingChanged;
bool tagsChanged;
template <class T> void loadWithInterval(const T &data, T &storage, T &highestStorage, MetadataHub::Status &status);
template <class T> void loadSingleValue(const T &data, T &storage, MetadataHub::Status &status);
};
MetadataWriteSettings::MetadataWriteSettings()
{
saveComments = false;
saveDateTime = false;
saveRating = false;
saveIptcTags = false;
saveIptcPhotographerId = false;
saveIptcCredits = false;
}
MetadataWriteSettings::MetadataWriteSettings(AlbumSettings *albumSettings)
{
saveComments = albumSettings->getSaveComments();
saveDateTime = albumSettings->getSaveDateTime();
saveRating = albumSettings->getSaveRating();
saveIptcTags = albumSettings->getSaveIptcTags();
saveIptcPhotographerId = albumSettings->getSaveIptcPhotographerId();
saveIptcCredits = albumSettings->getSaveIptcCredits();
iptcAuthor = albumSettings->getIptcAuthor();
iptcAuthorTitle = albumSettings->getIptcAuthorTitle();
iptcCredit = albumSettings->getIptcCredit();
iptcSource = albumSettings->getIptcSource();
iptcCopyright = albumSettings->getIptcCopyright();
}
MetadataHub::MetadataHub(DatabaseMode dbmode)
{
d = new MetadataHubPriv;
d->dbmode = dbmode;
}
MetadataHub::~MetadataHub()
{
delete d;
}
MetadataHub::MetadataHub(const MetadataHub &other)
{
d = new MetadataHubPriv(*other.d);
}
MetadataHub &MetadataHub::operator=(const MetadataHub &other)
{
(*d) = (*other.d);
return *this;
}
void MetadataHub::reset()
{
(*d) = MetadataHubPriv();
}
// --------------------------------------------------
void MetadataHub::load(ImageInfo *info)
{
d->count++;
load(info->dateTime(), info->caption(), info->rating());
AlbumManager *man = AlbumManager::instance();
TQValueList<int> tagIDs = info->tagIDs();
TQValueList<TAlbum *> loadedTags;
if (d->dbmode == ManagedTags)
{
TQValueList<TAlbum *> loadedTags;
for (TQValueList<int>::iterator it = tagIDs.begin(); it != tagIDs.end(); ++it)
{
TAlbum *album = man->findTAlbum(*it);
if (!album)
{
DWarning() << k_funcinfo << "Tag id " << *it << " not found in database." << endl;
continue;
}
loadedTags.append(album);
}
loadTags(loadedTags);
}
else
{
loadTags(info->tagPaths(false));
}
}
void MetadataHub::load(const DMetadata &metadata)
{
d->count++;
TQString comment;
TQStringList keywords;
TQDateTime datetime;
int rating;
// Try to get comments from image :
// In first, from standard JPEG comments, or
// In second, from EXIF comments tag, or
// In third, from IPTC comments tag.
comment = metadata.getImageComment();
// Try to get date and time from image :
// In first, from EXIF date & time tags, or
// In second, from IPTC date & time tags.
datetime = metadata.getImageDateTime();
// Try to get image rating from IPTC Urgency tag
// else use file system time stamp.
rating = metadata.getImageRating();
if ( !datetime.isValid() )
{
TQFileInfo info( metadata.getFilePath() );
datetime = info.lastModified();
}
load(datetime, comment, rating);
// Try to get image tags from IPTC keywords tags.
if (d->dbmode == ManagedTags)
{
AlbumManager *man = AlbumManager::instance();
TQStringList tagPaths = metadata.getImageKeywords();
TQValueList<TAlbum *> loadedTags;
for (TQStringList::iterator it = tagPaths.begin(); it != tagPaths.end(); ++it)
{
TAlbum *album = man->findTAlbum(*it);
if (!album)
{
DWarning() << k_funcinfo << "Tag id " << *it << " not found in database. Use NewTagsImport mode?" << endl;
continue;
}
loadedTags.append(album);
}
loadTags(loadedTags);
}
else
{
loadTags(metadata.getImageKeywords());
}
}
bool MetadataHub::load(const TQString &filePath)
{
DMetadata metadata;
bool success = metadata.load(filePath);
load(metadata); // increments count
return success;
}
// private common code to merge tags
void MetadataHub::loadTags(const TQValueList<TAlbum *> &loadedTags)
{
// get copy of tags
TQValueList<TAlbum *> previousTags = d->tags.keys();
// first go through all tags contained in this set
for (TQValueList<TAlbum *>::const_iterator it = loadedTags.begin(); it != loadedTags.end(); ++it)
{
// that is a reference
TagStatus &status = d->tags[*it];
// if it was not contained in the list, the default constructor will mark it as invalid
if (status == MetadataInvalid)
{
if (d->count == 1)
// there were no previous sets that could have contained the set
status = TagStatus(MetadataAvailable, true);
else
// previous sets did not contain the tag, we do => disjoint
status = TagStatus(MetadataDisjoint, true);
}
else if (status == TagStatus(MetadataAvailable, false))
{
// set to explicitly not contained, but we contain it => disjoint
status = TagStatus(MetadataDisjoint, true);
}
// else if mapIt.data() == MetadataAvailable, true: all right, we contain it too
// else if mapIt.data() == MetadataDisjoint: it's already disjoint
// remove from the list to signal that this tag has been handled
previousTags.remove(*it);
}
// Those tags which had been set as MetadataAvailable before,
// but are not contained in this set, have to be set to MetadataDisjoint
for (TQValueList<TAlbum *>::iterator it = previousTags.begin(); it != previousTags.end(); ++it)
{
TQMap<TAlbum *, TagStatus>::iterator mapIt = d->tags.find(*it);
if (mapIt != d->tags.end() && mapIt.data() == TagStatus(MetadataAvailable, true))
{
mapIt.data() = TagStatus(MetadataDisjoint, true);
}
}
}
// private code to merge tags with d->tagList
void MetadataHub::loadTags(const TQStringList &loadedTagPaths)
{
if (d->count == 1)
{
d->tagList = loadedTagPaths;
}
else
{
// a simple intersection
TQStringList toBeAdded;
for (TQStringList::iterator it = d->tagList.begin(); it != d->tagList.end(); ++it)
{
TQStringList::const_iterator newTagListIt = loadedTagPaths.find(*it);
if (newTagListIt == loadedTagPaths.end())
{
// it's not in the loadedTagPaths list. Remove it from intersection list.
it = d->tagList.remove(it);
}
// else, it is in both lists, so no need to change d->tagList, it's already added.
}
}
}
// private common code to load dateTime, comment, rating
void MetadataHub::load(const TQDateTime &dateTime, const TQString &comment, int rating)
{
if (dateTime.isValid())
{
d->loadWithInterval<TQDateTime>(dateTime, d->dateTime, d->lastDateTime, d->dateTimeStatus);
}
d->loadWithInterval<int>(rating, d->rating, d->highestRating, d->ratingStatus);
d->loadSingleValue<TQString>(comment, d->comment, d->commentStatus);
}
// template method to share code for dateTime and rating
template <class T> void MetadataHubPriv::loadWithInterval(const T &data, T &storage, T &highestStorage, MetadataHub::Status &status)
{
switch (status)
{
case MetadataHub::MetadataInvalid:
storage = data;
status = MetadataHub::MetadataAvailable;
break;
case MetadataHub::MetadataAvailable:
// we have two values. If they are equal, status is unchanged
if (data == storage)
break;
// they are not equal. We need to enter the disjoint state.
status = MetadataHub::MetadataDisjoint;
if (data > storage)
{
highestStorage = data;
}
else
{
highestStorage = storage;
storage = data;
}
break;
case MetadataHub::MetadataDisjoint:
// smaller value is stored in storage
if (data < storage)
storage = data;
else if (highestStorage < data)
highestStorage = data;
break;
}
}
// template method used by comment
template <class T> void MetadataHubPriv::loadSingleValue(const T &data, T &storage, MetadataHub::Status &status)
{
switch (status)
{
case MetadataHub::MetadataInvalid:
storage = data;
status = MetadataHub::MetadataAvailable;
break;
case MetadataHub::MetadataAvailable:
// we have two values. If they are equal, status is unchanged
if (data == storage)
break;
// they are not equal. We need to enter the disjoint state.
status = MetadataHub::MetadataDisjoint;
break;
case MetadataHub::MetadataDisjoint:
break;
}
}
// --------------------------------------------------
bool MetadataHub::write(ImageInfo *info, WriteMode writeMode)
{
bool changed = false;
// find out in advance if we have something to write - needed for FullWriteIfChanged mode
bool saveComment = d->commentStatus == MetadataAvailable;
bool saveDateTime = d->dateTimeStatus == MetadataAvailable;
bool saveRating = d->ratingStatus == MetadataAvailable;
bool saveTags = false;
for (TQMap<TAlbum *, TagStatus>::iterator it = d->tags.begin(); it != d->tags.end(); ++it)
{
if (it.data() == MetadataAvailable)
{
saveTags = true;
break;
}
}
bool writeAllFields;
if (writeMode == FullWrite)
writeAllFields = true;
else if (writeMode == FullWriteIfChanged)
writeAllFields = (
(saveComment && d->commentChanged) ||
(saveDateTime && d->dateTimeChanged) ||
(saveRating && d->ratingChanged) ||
(saveTags && d->tagsChanged)
);
else // PartialWrite
writeAllFields = false;
if (saveComment && (writeAllFields || d->commentChanged))
{
info->setCaption(d->comment);
changed = true;
}
if (saveDateTime && (writeAllFields || d->dateTimeChanged))
{
info->setDateTime(d->dateTime);
changed = true;
}
if (saveRating && (writeAllFields || d->ratingChanged))
{
info->setRating(d->rating);
changed = true;
}
if (writeAllFields || d->tagsChanged)
{
if (d->dbmode == ManagedTags)
{
for (TQMap<TAlbum *, TagStatus>::iterator it = d->tags.begin(); it != d->tags.end(); ++it)
{
if (it.data() == MetadataAvailable)
{
if (it.data().hasTag)
info->setTag(it.key()->id());
else
info->removeTag(it.key()->id());
changed = true;
}
}
}
else
{
// tags not yet contained in database will be created
info->addTagPaths(d->tagList);
changed = changed || !d->tagList.isEmpty();
}
}
return changed;
}
bool MetadataHub::write(DMetadata &metadata, WriteMode writeMode, const MetadataWriteSettings &settings)
{
bool dirty = false;
// find out in advance if we have something to write - needed for FullWriteIfChanged mode
bool saveComment = (settings.saveComments && d->commentStatus == MetadataAvailable);
bool saveDateTime = (settings.saveDateTime && d->dateTimeStatus == MetadataAvailable);
bool saveRating = (settings.saveRating && d->ratingStatus == MetadataAvailable);
bool saveTags = false;
if (settings.saveIptcTags)
{
saveTags = false;
// find at least one tag to write
for (TQMap<TAlbum *, TagStatus>::iterator it = d->tags.begin(); it != d->tags.end(); ++it)
{
if (it.data() == MetadataAvailable)
{
saveTags = true;
break;
}
}
}
bool writeAllFields;
if (writeMode == FullWrite)
writeAllFields = true;
else if (writeMode == FullWriteIfChanged)
writeAllFields = (
(saveComment && d->commentChanged) ||
(saveDateTime && d->dateTimeChanged) ||
(saveRating && d->ratingChanged) ||
(saveTags && d->tagsChanged)
);
else // PartialWrite
writeAllFields = false;
if (saveComment && (writeAllFields || d->commentChanged))
{
// Store comments in image as JFIF comments, Exif comments, and Iptc Comments.
dirty |= metadata.setImageComment(d->comment);
}
if (saveDateTime && (writeAllFields || d->dateTimeChanged))
{
// Store Image Date & Time as Exif and Iptc tags.
dirty |= metadata.setImageDateTime(d->dateTime, false);
}
if (saveRating && (writeAllFields || d->ratingChanged))
{
// Store Image rating as Iptc tag.
dirty |= metadata.setImageRating(d->rating);
}
if (saveTags && (writeAllFields || d->tagsChanged))
{
// Store tag paths as Iptc keywords tags.
// DatabaseMode == ManagedTags is assumed.
// To fix this constraint (not needed currently), an oldKeywords parameter is needed
// create list of keywords to be added and to be removed
TQStringList newKeywords, oldKeywords;
for (TQMap<TAlbum *, TagStatus>::iterator it = d->tags.begin(); it != d->tags.end(); ++it)
{
// it is important that MetadataDisjoint keywords are not touched
if (it.data() == MetadataAvailable)
{
// This works for single and multiple selection.
// In both situations, tags which had originally been loaded
// have explicitly been removed with setTag.
if (it.data().hasTag)
newKeywords.append(it.key()->tagPath(false));
else
oldKeywords.append(it.key()->tagPath(false));
}
}
// NOTE: See B.K.O #175321 : we remove all old keyword from IPTC and XMP before to
// synchronize metadata, else contents is not coherent.
dirty |= metadata.setImageKeywords(metadata.getImageKeywords(), newKeywords);
}
if (settings.saveIptcPhotographerId && writeAllFields)
{
// Store Photograph identity into the Iptc tags.
dirty |= metadata.setImagePhotographerId(settings.iptcAuthor,
settings.iptcAuthorTitle);
}
if (settings.saveIptcCredits && writeAllFields)
{
// Store Photograph identity into the Iptc tags.
dirty |= metadata.setImageCredits(settings.iptcCredit,
settings.iptcSource,
settings.iptcCopyright);
}
return dirty;
}
bool MetadataHub::write(const TQString &filePath, WriteMode writeMode, const MetadataWriteSettings &settings)
{
// if no DMetadata object is needed at all, don't construct one -
// important optimization if writing to file is turned off in setup!
if (!needWriteMetadata(writeMode, settings))
return false;
DMetadata metadata(filePath);
if (write(metadata, writeMode, settings))
{
bool success = metadata.applyChanges();
ImageAttributesWatch::instance()->fileMetadataChanged(filePath);
return success;
}
return false;
}
bool MetadataHub::write(DImg &image, WriteMode writeMode, const MetadataWriteSettings &settings)
{
// if no DMetadata object is needed at all, don't construct one
if (!needWriteMetadata(writeMode, settings))
return false;
// See DImgLoader::readMetadata() and saveMetadata()
DMetadata metadata;
metadata.setComments(image.getComments());
metadata.setExif(image.getExif());
metadata.setIptc(image.getIptc());
if (write(metadata, writeMode, settings))
{
// Do not insert null data into metaData map:
// Even if byte array is null, if there is a key in the map, it will
// be interpreted as "There was data, so write it again to the file".
if (!metadata.getComments().isNull())
image.setComments(metadata.getComments());
if (!metadata.getExif().isNull())
image.setExif(metadata.getExif());
if (!metadata.getIptc().isNull())
image.setIptc(metadata.getIptc());
return true;
}
return false;
}
bool MetadataHub::needWriteMetadata(WriteMode writeMode, const MetadataWriteSettings &settings) const
{
// This is the same logic as in write(DMetadata) but without actually writing.
// Adapt if the method above changes
bool saveComment = (settings.saveComments && d->commentStatus == MetadataAvailable);
bool saveDateTime = (settings.saveDateTime && d->dateTimeStatus == MetadataAvailable);
bool saveRating = (settings.saveRating && d->ratingStatus == MetadataAvailable);
bool saveTags = false;
if (settings.saveIptcTags)
{
saveTags = false;
// find at least one tag to write
for (TQMap<TAlbum *, TagStatus>::iterator it = d->tags.begin(); it != d->tags.end(); ++it)
{
if (it.data() == MetadataAvailable)
{
saveTags = true;
break;
}
}
}
bool writeAllFields;
if (writeMode == FullWrite)
writeAllFields = true;
else if (writeMode == FullWriteIfChanged)
writeAllFields = (
(saveComment && d->commentChanged) ||
(saveDateTime && d->dateTimeChanged) ||
(saveRating && d->ratingChanged) ||
(saveTags && d->tagsChanged)
);
else // PartialWrite
writeAllFields = false;
return (
(saveComment && (writeAllFields || d->commentChanged)) ||
(saveDateTime && (writeAllFields || d->dateTimeChanged)) ||
(saveRating && (writeAllFields || d->ratingChanged)) ||
(saveTags && (writeAllFields || d->tagsChanged)) ||
(settings.saveIptcPhotographerId && writeAllFields) ||
(settings.saveIptcCredits && writeAllFields)
);
}
MetadataWriteSettings MetadataHub::defaultWriteSettings()
{
if (AlbumSettings::instance())
return MetadataWriteSettings(AlbumSettings::instance());
else
// is this check necessary?
return MetadataWriteSettings();
}
// --------------------------------------------------
MetadataHub::Status MetadataHub::dateTimeStatus() const
{
return d->dateTimeStatus;
}
MetadataHub::Status MetadataHub::commentStatus() const
{
return d->commentStatus;
}
MetadataHub::Status MetadataHub::ratingStatus() const
{
return d->ratingStatus;
}
MetadataHub::TagStatus MetadataHub::tagStatus(int albumId) const
{
if (d->dbmode == NewTagsImport)
return TagStatus(MetadataInvalid);
return tagStatus(AlbumManager::instance()->findTAlbum(albumId));
}
MetadataHub::TagStatus MetadataHub::tagStatus(const TQString &tagPath) const
{
if (d->dbmode == NewTagsImport)
return TagStatus(MetadataInvalid);
return tagStatus(AlbumManager::instance()->findTAlbum(tagPath));
}
MetadataHub::TagStatus MetadataHub::tagStatus(TAlbum *album) const
{
if (!album)
return TagStatus(MetadataInvalid);
TQMap<TAlbum *, TagStatus>::iterator mapIt = d->tags.find(album);
if (mapIt == d->tags.end())
return TagStatus(MetadataInvalid);
return mapIt.data();
}
bool MetadataHub::dateTimeChanged() const
{
return d->dateTimeChanged;
}
bool MetadataHub::commentChanged() const
{
return d->commentChanged;
}
bool MetadataHub::ratingChanged() const
{
return d->ratingChanged;
}
bool MetadataHub::tagsChanged() const
{
return d->tagsChanged;
}
TQDateTime MetadataHub::dateTime() const
{
return d->dateTime;
}
TQString MetadataHub::comment() const
{
return d->comment;
}
int MetadataHub::rating() const
{
return d->rating;
}
void MetadataHub::dateTimeInterval(TQDateTime &lowest, TQDateTime &highest) const
{
switch (d->dateTimeStatus)
{
case MetadataInvalid:
lowest = highest = TQDateTime();
break;
case MetadataAvailable:
lowest = highest = d->dateTime;
break;
case MetadataDisjoint:
lowest = d->dateTime;
highest = d->lastDateTime;
break;
}
}
void MetadataHub::ratingInterval(int &lowest, int &highest) const
{
switch (d->ratingStatus)
{
case MetadataInvalid:
lowest = highest = -1;
break;
case MetadataAvailable:
lowest = highest = d->rating;
break;
case MetadataDisjoint:
lowest = d->rating;
highest = d->highestRating;
break;
}
}
TQStringList MetadataHub::keywords() const
{
if (d->dbmode == NewTagsImport)
return d->tagList;
else
{
TQStringList tagList;
for (TQMap<TAlbum *, TagStatus>::iterator it = d->tags.begin(); it != d->tags.end(); ++it)
{
if (it.data() == TagStatus(MetadataAvailable, true))
tagList.append(it.key()->tagPath(false));
}
return tagList;
}
}
TQMap<TAlbum *, MetadataHub::TagStatus> MetadataHub::tags() const
{
// DatabaseMode == ManagedTags is assumed
return d->tags;
}
TQMap<int, MetadataHub::TagStatus> MetadataHub::tagIDs() const
{
// DatabaseMode == ManagedTags is assumed
TQMap<int, TagStatus> intmap;
for (TQMap<TAlbum *, TagStatus>::iterator it = d->tags.begin(); it != d->tags.end(); ++it)
{
intmap.insert(it.key()->id(), it.data());
}
return intmap;
}
// --------------------------------------------------
void MetadataHub::setDateTime(const TQDateTime &dateTime, Status status)
{
d->dateTimeStatus = status;
d->dateTime = dateTime;
d->dateTimeChanged = true;
}
void MetadataHub::setComment(const TQString &comment, Status status)
{
d->commentStatus = status;
d->comment = comment;
d->commentChanged = true;
}
void MetadataHub::setRating(int rating, Status status)
{
d->ratingStatus = status;
d->rating = rating;
d->ratingChanged = true;
}
void MetadataHub::setTag(TAlbum *tag, bool hasTag, Status status)
{
// DatabaseMode == ManagedTags is assumed
d->tags[tag] = TagStatus(status, hasTag);
d->tagsChanged = true;
}
void MetadataHub::setTag(int albumID, bool hasTag, Status status)
{
// DatabaseMode == ManagedTags is assumed
TAlbum *album = AlbumManager::instance()->findTAlbum(albumID);
if (!album)
{
DWarning() << k_funcinfo << "Tag ID " << albumID << " not found in database." << endl;
return;
}
setTag(album, hasTag, status);
}
void MetadataHub::resetChanged()
{
d->dateTimeChanged = false;
d->commentChanged = false;
d->ratingChanged = false;
d->tagsChanged = false;
}
} // namespace Digikam