/* ============================================================ * * 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 * Copyright (C) 2007-2009 by Gilles Caulier * * 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 // 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() { dateTimetqStatus = MetadataHub::MetadataInvalid; ratingtqStatus = MetadataHub::MetadataInvalid; commenttqStatus = MetadataHub::MetadataInvalid; rating = -1; highestRating = -1; count = 0; dbmode = MetadataHub::ManagedTags; dateTimeChanged = false; commentChanged = false; ratingChanged = false; tagsChanged = false; } MetadataHub::tqStatus dateTimetqStatus; MetadataHub::tqStatus commenttqStatus; MetadataHub::tqStatus ratingtqStatus; TQDateTime dateTime; TQDateTime lastDateTime; TQString comment; int rating; int highestRating; int count; TQMap tags; TQStringList tagList; MetadataHub::DatabaseMode dbmode; bool dateTimeChanged; bool commentChanged; bool ratingChanged; bool tagsChanged; template void loadWithInterval(const T &data, T &storage, T &highestStorage, MetadataHub::tqStatus &status); template void loadSingleValue(const T &data, T &storage, MetadataHub::tqStatus &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 tagIDs = info->tagIDs(); TQValueList loadedTags; if (d->dbmode == ManagedTags) { TQValueList loadedTags; for (TQValueList::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 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 &loadedTags) { // get copy of tags TQValueList previousTags = d->tags.keys(); // first go through all tags contained in this set for (TQValueList::const_iterator it = loadedTags.begin(); it != loadedTags.end(); ++it) { // that is a reference TagtqStatus &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 = TagtqStatus(MetadataAvailable, true); else // previous sets did not contain the tag, we do => disjoint status = TagtqStatus(MetadataDisjoint, true); } else if (status == TagtqStatus(MetadataAvailable, false)) { // set to explicitly not contained, but we contain it => disjoint status = TagtqStatus(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::iterator it = previousTags.begin(); it != previousTags.end(); ++it) { TQMap::iterator mapIt = d->tags.find(*it); if (mapIt != d->tags.end() && mapIt.data() == TagtqStatus(MetadataAvailable, true)) { mapIt.data() = TagtqStatus(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(dateTime, d->dateTime, d->lastDateTime, d->dateTimetqStatus); } d->loadWithInterval(rating, d->rating, d->highestRating, d->ratingtqStatus); d->loadSingleValue(comment, d->comment, d->commenttqStatus); } // template method to share code for dateTime and rating template void MetadataHubPriv::loadWithInterval(const T &data, T &storage, T &highestStorage, MetadataHub::tqStatus &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 void MetadataHubPriv::loadSingleValue(const T &data, T &storage, MetadataHub::tqStatus &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->commenttqStatus == MetadataAvailable; bool saveDateTime = d->dateTimetqStatus == MetadataAvailable; bool saveRating = d->ratingtqStatus == MetadataAvailable; bool saveTags = false; for (TQMap::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::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->commenttqStatus == MetadataAvailable); bool saveDateTime = (settings.saveDateTime && d->dateTimetqStatus == MetadataAvailable); bool saveRating = (settings.saveRating && d->ratingtqStatus == MetadataAvailable); bool saveTags = false; if (settings.saveIptcTags) { saveTags = false; // find at least one tag to write for (TQMap::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::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->commenttqStatus == MetadataAvailable); bool saveDateTime = (settings.saveDateTime && d->dateTimetqStatus == MetadataAvailable); bool saveRating = (settings.saveRating && d->ratingtqStatus == MetadataAvailable); bool saveTags = false; if (settings.saveIptcTags) { saveTags = false; // find at least one tag to write for (TQMap::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::tqStatus MetadataHub::dateTimetqStatus() const { return d->dateTimetqStatus; } MetadataHub::tqStatus MetadataHub::commenttqStatus() const { return d->commenttqStatus; } MetadataHub::tqStatus MetadataHub::ratingtqStatus() const { return d->ratingtqStatus; } MetadataHub::TagtqStatus MetadataHub::tagtqStatus(int albumId) const { if (d->dbmode == NewTagsImport) return TagtqStatus(MetadataInvalid); return tagtqStatus(AlbumManager::instance()->findTAlbum(albumId)); } MetadataHub::TagtqStatus MetadataHub::tagtqStatus(const TQString &tagPath) const { if (d->dbmode == NewTagsImport) return TagtqStatus(MetadataInvalid); return tagtqStatus(AlbumManager::instance()->findTAlbum(tagPath)); } MetadataHub::TagtqStatus MetadataHub::tagtqStatus(TAlbum *album) const { if (!album) return TagtqStatus(MetadataInvalid); TQMap::iterator mapIt = d->tags.find(album); if (mapIt == d->tags.end()) return TagtqStatus(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->dateTimetqStatus) { 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->ratingtqStatus) { 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::iterator it = d->tags.begin(); it != d->tags.end(); ++it) { if (it.data() == TagtqStatus(MetadataAvailable, true)) tagList.append(it.key()->tagPath(false)); } return tagList; } } TQMap MetadataHub::tags() const { // DatabaseMode == ManagedTags is assumed return d->tags; } TQMap MetadataHub::tagIDs() const { // DatabaseMode == ManagedTags is assumed TQMap intmap; for (TQMap::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, tqStatus status) { d->dateTimetqStatus = status; d->dateTime = dateTime; d->dateTimeChanged = true; } void MetadataHub::setComment(const TQString &comment, tqStatus status) { d->commenttqStatus = status; d->comment = comment; d->commentChanged = true; } void MetadataHub::setRating(int rating, tqStatus status) { d->ratingtqStatus = status; d->rating = rating; d->ratingChanged = true; } void MetadataHub::setTag(TAlbum *tag, bool hasTag, tqStatus status) { // DatabaseMode == ManagedTags is assumed d->tags[tag] = TagtqStatus(status, hasTag); d->tagsChanged = true; } void MetadataHub::setTag(int albumID, bool hasTag, tqStatus 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