diff --git a/libs/database/coredb/coredbfields.h b/libs/database/coredb/coredbfields.h index 99265c1424..ca390fbc01 100644 --- a/libs/database/coredb/coredbfields.h +++ b/libs/database/coredb/coredbfields.h @@ -1,592 +1,592 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2007-09-22 * Description : Core database field enumerations. * * Copyright (C) 2007-2012 by Marcel Wiesweg * Copyright (C) 2011-2018 by Gilles Caulier * Copyright (C) 2013 by Michael G. Hansen * * 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. * * ============================================================ */ #ifndef COREDATABASEFIELDS_H #define COREDATABASEFIELDS_H #include "digikam_config.h" // C++ includes #include // Qt includes #include #include // Local includes #include "digikam_export.h" #ifdef HAVE_DBUS class QDBusArgument; #endif namespace Digikam { namespace DatabaseFields { enum ImagesField { ImagesNone = 0, Album = 1 << 0, Name = 1 << 1, Status = 1 << 2, Category = 1 << 3, ModificationDate = 1 << 4, FileSize = 1 << 5, UniqueHash = 1 << 6, ImagesAll = Album | Name | Status | Category | ModificationDate | FileSize | UniqueHash, ImagesFirst = Album, ImagesLast = UniqueHash }; typedef uint8_t ImagesMinSizeType; enum ImageInformationField { ImageInformationNone = 0, Rating = 1 << 0, CreationDate = 1 << 1, DigitizationDate = 1 << 2, Orientation = 1 << 3, Width = 1 << 4, Height = 1 << 5, Format = 1 << 6, ColorDepth = 1 << 7, ColorModel = 1 << 8, ColorLabel = 1 << 9, PickLabel = 1 << 10, ImageInformationAll = Rating | CreationDate | DigitizationDate | Orientation | Width | Height | Format | ColorDepth | ColorModel | ColorLabel | PickLabel, ImageInformationFirst = Rating, ImageInformationLast = PickLabel }; typedef uint16_t ImageInformationMinSizeType; enum ImageMetadataField { ImageMetadataNone = 0, Make = 1 << 0, Model = 1 << 1, Lens = 1 << 2, Aperture = 1 << 3, FocalLength = 1 << 4, FocalLength35 = 1 << 5, ExposureTime = 1 << 6, ExposureProgram = 1 << 7, ExposureMode = 1 << 8, Sensitivity = 1 << 9, FlashMode = 1 << 10, WhiteBalance = 1 << 11, WhiteBalanceColorTemperature = 1 << 12, MeteringMode = 1 << 13, SubjectDistance = 1 << 14, SubjectDistanceCategory = 1 << 15, ImageMetadataAll = Make | Model | Lens | Aperture | FocalLength | FocalLength35 | ExposureTime | ExposureProgram | ExposureMode | Sensitivity | FlashMode | WhiteBalance | WhiteBalanceColorTemperature | MeteringMode | SubjectDistance | SubjectDistanceCategory, ImageMetadataFirst = Make, ImageMetadataLast = SubjectDistanceCategory }; typedef uint16_t ImageMetadataMinSizeType; enum ImagePositionsField { ImagePositionsNone = 0, Latitude = 1 << 0, LatitudeNumber = 1 << 1, Longitude = 1 << 2, LongitudeNumber = 1 << 3, Altitude = 1 << 4, PositionOrientation = 1 << 5, PositionTilt = 1 << 6, PositionRoll = 1 << 7, PositionAccuracy = 1 << 8, PositionDescription = 1 << 9, ImagePositionsAll = Latitude | LatitudeNumber | Longitude | LongitudeNumber | Altitude | PositionOrientation | PositionRoll | PositionTilt | PositionAccuracy | PositionDescription, ImagePositionsFirst = Latitude, ImagePositionsLast = PositionDescription }; typedef uint16_t ImagePositionsMinSizeType; enum ImageCommentsField { ImageCommentsNone = 0, CommentType = 1 << 0, CommentLanguage = 1 << 1, CommentAuthor = 1 << 2, CommentDate = 1 << 3, Comment = 1 << 4, ImageCommentsAll = CommentType | CommentAuthor | CommentLanguage | CommentDate | Comment, ImageCommentsFirst = CommentType, ImageCommentsLast = Comment }; typedef uint8_t ImageCommentsMinSizeType; enum ImageHistoryInfoField { ImageHistoryInfoNone = 0, ImageUUID = 1 << 0, ImageHistory = 1 << 1, ImageRelations = 1 << 2, ImageHistoryInfoAll = ImageUUID | ImageHistory | ImageRelations, ImageHistoryInfoFirst = ImageUUID, ImageHistoryInfoLast = ImageRelations }; typedef uint8_t ImageHistoryInfoMinSizeType; enum VideoMetadataField { VideoMetadataNone = 0, AspectRatio = 1 << 0, AudioBitRate = 1 << 1, AudioChannelType = 1 << 2, - AudioCodec = 1 << 3, + AudioCodec = 1 << 3, Duration = 1 << 4, FrameRate = 1 << 5, VideoCodec = 1 << 6, VideoMetadataAll = AspectRatio | AudioBitRate | AudioChannelType | AudioCodec | Duration | FrameRate | VideoCodec, VideoMetadataFirst = AspectRatio, VideoMetadataLast = VideoCodec }; typedef uint8_t VideoMetadataMinSizeType; Q_DECLARE_FLAGS(Images, ImagesField) Q_DECLARE_FLAGS(ImageInformation, ImageInformationField) Q_DECLARE_FLAGS(ImageMetadata, ImageMetadataField) Q_DECLARE_FLAGS(ImageComments, ImageCommentsField) Q_DECLARE_FLAGS(ImagePositions, ImagePositionsField) Q_DECLARE_FLAGS(ImageHistoryInfo, ImageHistoryInfoField) Q_DECLARE_FLAGS(VideoMetadata, VideoMetadataField) template class FieldMetaInfo { }; #define DECLARE_FIELDMETAINFO(FieldName) \ template<> class DIGIKAM_DATABASE_EXPORT FieldMetaInfo \ { \ public: \ static const FieldName##Field DIGIKAM_DATABASE_EXPORT First = FieldName##First; \ static const FieldName##Field DIGIKAM_DATABASE_EXPORT Last = FieldName##Last; \ typedef FieldName##MinSizeType MinSizeType; \ inline static MinSizeType toMinSizeType(const FieldName value) { return MinSizeType(value); } \ inline static FieldName fromMinSizeType(const MinSizeType value) { return FieldName(value); } \ }; DECLARE_FIELDMETAINFO(Images) DECLARE_FIELDMETAINFO(ImageInformation) DECLARE_FIELDMETAINFO(ImageMetadata) DECLARE_FIELDMETAINFO(ImageComments) DECLARE_FIELDMETAINFO(ImagePositions) DECLARE_FIELDMETAINFO(ImageHistoryInfo) DECLARE_FIELDMETAINFO(VideoMetadata) /** * You can iterate over each of the Enumerations defined above: * ImagesIterator, ImageMetadataIterator etc. * for (ImagesIterator it; !it.atEnd(); ++it) {} */ template class DatabaseFieldsEnumIterator { public: DatabaseFieldsEnumIterator() : i(FieldMetaInfo::First) { } bool atEnd() const { return i > FieldMetaInfo::Last; } void operator++() { i = (i << 1); } FieldName operator*() const { return FieldName(i); } private: int i; }; /** * An iterator that iterates only over the flags which are set */ template class DatabaseFieldsEnumIteratorSetOnly { public: DatabaseFieldsEnumIteratorSetOnly(const FieldName setValues) : i(), values(setValues) { if (! (*i & values) ) { operator++(); } } bool atEnd() const { return i.atEnd(); } void operator++() { while (!i.atEnd()) { ++i; if (*i & values) { break; } } } FieldName operator*() const { return *i; } private: DatabaseFieldsEnumIterator i; const FieldName values; }; #define DATABASEFIELDS_ENUM_ITERATOR(Flag) \ typedef DatabaseFieldsEnumIterator Flag##Iterator; \ typedef DatabaseFieldsEnumIteratorSetOnly Flag##IteratorSetOnly; DATABASEFIELDS_ENUM_ITERATOR(Images) DATABASEFIELDS_ENUM_ITERATOR(ImageInformation) DATABASEFIELDS_ENUM_ITERATOR(ImageMetadata) DATABASEFIELDS_ENUM_ITERATOR(VideoMetadata) DATABASEFIELDS_ENUM_ITERATOR(ImagePositions) DATABASEFIELDS_ENUM_ITERATOR(ImageComments) DATABASEFIELDS_ENUM_ITERATOR(ImageHistoryInfo) /** * For your custom enum, you need to use the CustomEnum class. * You need to do an explicit cast. */ enum CustomEnumFlags { }; Q_DECLARE_FLAGS(CustomEnum, CustomEnumFlags) #define DATABASEFIELDS_SET_DECLARE_METHODS(Flag, variable) \ Set(const Flag& f) { initialize(); variable = f; } \ Set(const Flag##Field& f) { initialize(); variable = f; } \ inline Flag& operator=(const Flag& f) { return variable.operator=(f); } \ inline Flag& operator|=(Flag f) { return variable.operator|=(f); } \ inline Flag& operator^=(Flag f) { return variable.operator^=(f); } \ inline Flag operator|(Flag f) const { return variable.operator|(f); } \ inline Flag operator^(Flag f) const { return variable.operator^(f); } \ inline Flag operator&(Flag f) const { return variable.operator&(f); } \ inline operator Flag() const { return variable; } \ inline bool hasFieldsFrom##Flag() const { return variable & Flag##All; } \ inline Flag get##Flag() const { return variable; } /** * This class provides a set of all DatabasFields enums, * without resorting to a QSet. */ class Set { public: Set() { initialize(); } void initialize() { images = ImagesNone; imageInformation = ImageInformationNone; imageMetadata = ImageMetadataNone; imageComments = ImageCommentsNone; imagePositions = ImagePositionsNone; imageHistory = ImageHistoryInfoNone; videoMetadata = VideoMetadataNone; customEnum = (CustomEnum)0; } public: DATABASEFIELDS_SET_DECLARE_METHODS(Images, images) DATABASEFIELDS_SET_DECLARE_METHODS(ImageInformation, imageInformation) DATABASEFIELDS_SET_DECLARE_METHODS(VideoMetadata, videoMetadata) DATABASEFIELDS_SET_DECLARE_METHODS(ImageMetadata, imageMetadata) DATABASEFIELDS_SET_DECLARE_METHODS(ImageComments, imageComments) DATABASEFIELDS_SET_DECLARE_METHODS(ImagePositions, imagePositions) DATABASEFIELDS_SET_DECLARE_METHODS(ImageHistoryInfo, imageHistory) inline bool operator&(const Set& other) { return (images & other.images) || (imageInformation & other.imageInformation) || (imageMetadata & other.imageMetadata) || (imageComments & other.imageComments) || (imagePositions & other.imagePositions) || (imageHistory & other.imageHistory) || (customEnum & other.customEnum) || (videoMetadata & other.videoMetadata); } // overloading operator|= creates ambiguity with the database fields' // operator|=, therefore we give it another name. inline Set& setFields(const Set& otherSet) { images|= otherSet.images; imageInformation|= otherSet.imageInformation; imageMetadata|= otherSet.imageMetadata; imageComments|= otherSet.imageComments; imagePositions|= otherSet.imagePositions; imageHistory|= otherSet.imageHistory; customEnum|= otherSet.customEnum; videoMetadata|= otherSet.videoMetadata; return *this; } inline CustomEnum& operator=(const CustomEnum& f) { return customEnum.operator=(f); } inline CustomEnum& operator|=(CustomEnum f) { return customEnum.operator|=(f); } inline CustomEnum& operator^=(CustomEnum f) { return customEnum.operator^=(f); } inline CustomEnum operator|(CustomEnum f) const { return customEnum.operator|(f); } inline CustomEnum operator^(CustomEnum f) const { return customEnum.operator^(f); } inline CustomEnum operator&(CustomEnum f) const { return customEnum.operator&(f); } #ifdef HAVE_DBUS // databasechangesets.cpp Set& operator<<(const QDBusArgument& argument); const Set& operator>>(QDBusArgument& argument) const; #endif private: Images images; ImageInformation imageInformation; ImageMetadata imageMetadata; VideoMetadata videoMetadata; ImageComments imageComments; ImagePositions imagePositions; ImageHistoryInfo imageHistory; CustomEnum customEnum; }; #define DATABASEFIELDS_HASH_DECLARE_METHODS(Key, method) \ void insertField(const Key& key, const T& value) { QHash::insert(method(key), value); } \ int remove(const Key& key) { return QHash::remove(method(key)); } \ int removeAllFields(const Key& key) \ { \ int removedCount = 0; \ for (DatabaseFieldsEnumIteratorSetOnly it(key); !it.atEnd(); ++it) \ { \ removedCount+=remove(*it); \ } \ return removedCount; \ } \ T take(const Key& key) { return QHash::take(method(key)); } \ \ bool contains(const Key& key) const { return QHash::contains(method(key)); } \ const T value(const Key& key) const { return QHash::value(method(key)); } \ const T value(const Key& key, const T& defaultValue) const { return QHash::value(method(key), defaultValue); } \ \ T& operator[](const Key& key) { return QHash::operator[](method(key)); } \ const T operator[](const Key& key) const { return QHash::operator[](method(key)); } \ \ QList values(const Key& key) const { return QHash::value(method(key)); } \ int count(const Key& key) const { return QHash::count(method(key)); } /** * This class provides a hash on all DatabaseFields enums, * allowing to use the enum values as independent keys. * You can use the class like a normal QHash with the value type defined * by you, and as keys the members of the DatabaseFields enums. * You can only use single enum members as keys, not or'ed numbers. * You can use one custom enum, cast to DatabaseFields::CustomEnum, * which can have at most 26 flag values (1 << 0 to 1 << 26). * Pass this as the optional second template parameter. */ template class Hash : public QHash { public: // We use the upper 6 bits to distinguish the enums, and give the lower 26 bits to the flags. // So we can store up to 64 enums, with 26 flags each. static inline unsigned int uniqueKey(Images f) { return (int)f | (0 << 26); } static inline unsigned int uniqueKey(ImageInformation f) { return (int)f | (1 << 26); } static inline unsigned int uniqueKey(ImageMetadata f) { return (int)f | (2 << 26); } static inline unsigned int uniqueKey(ImageComments f) { return (int)f | (3 << 26); } static inline unsigned int uniqueKey(ImagePositions f) { return (int)f | (4 << 26); } static inline unsigned int uniqueKey(ImageHistoryInfo f) { return (int)f | (5 << 26); } static inline unsigned int uniqueKey(VideoMetadata f) { return (int)f | (6 << 26); } static inline unsigned int uniqueKey(CustomEnum f) { return f | (63 << 26); } // override relevant methods from QHash DATABASEFIELDS_HASH_DECLARE_METHODS(Images, uniqueKey); DATABASEFIELDS_HASH_DECLARE_METHODS(ImageInformation, uniqueKey); DATABASEFIELDS_HASH_DECLARE_METHODS(ImageMetadata, uniqueKey); DATABASEFIELDS_HASH_DECLARE_METHODS(VideoMetadata, uniqueKey); DATABASEFIELDS_HASH_DECLARE_METHODS(ImageComments, uniqueKey); DATABASEFIELDS_HASH_DECLARE_METHODS(ImagePositions, uniqueKey); DATABASEFIELDS_HASH_DECLARE_METHODS(ImageHistoryInfo, uniqueKey); DATABASEFIELDS_HASH_DECLARE_METHODS(CustomEnum, uniqueKey); }; } // end of namespace DatabaseFields } // end of namespace Digikam // must be outside the namespace! Q_DECLARE_OPERATORS_FOR_FLAGS(Digikam::DatabaseFields::Images) Q_DECLARE_OPERATORS_FOR_FLAGS(Digikam::DatabaseFields::ImageInformation) Q_DECLARE_OPERATORS_FOR_FLAGS(Digikam::DatabaseFields::ImageMetadata) Q_DECLARE_OPERATORS_FOR_FLAGS(Digikam::DatabaseFields::VideoMetadata) Q_DECLARE_OPERATORS_FOR_FLAGS(Digikam::DatabaseFields::ImageComments) Q_DECLARE_OPERATORS_FOR_FLAGS(Digikam::DatabaseFields::ImagePositions) Q_DECLARE_OPERATORS_FOR_FLAGS(Digikam::DatabaseFields::ImageHistoryInfo) #endif // COREDATABASEFIELDS_H diff --git a/libs/database/item/imagescanner.cpp b/libs/database/item/imagescanner.cpp index 97dca3519a..f4bcdd77d6 100644 --- a/libs/database/item/imagescanner.cpp +++ b/libs/database/item/imagescanner.cpp @@ -1,2021 +1,2027 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2007-09-19 * Description : Scanning of a single image * * Copyright (C) 2007-2013 by Marcel Wiesweg * Copyright (C) 2013-2018 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. * * ============================================================ */ #include "imagescanner.h" // Qt includes #include #include // KDE includes #include // Local includes #include "digikam_debug.h" #include "coredburl.h" #include "coredbaccess.h" #include "coredb.h" #include "similaritydbaccess.h" #include "similaritydb.h" #include "collectionlocation.h" #include "collectionmanager.h" #include "facetagseditor.h" #include "imagecomments.h" #include "imagecopyright.h" #include "imageextendedproperties.h" #include "imagehistorygraph.h" #include "metadatasettings.h" #include "tagregion.h" #include "tagscache.h" #include "iostream" #include "dimagehistory.h" #include "imagehistorygraphdata.h" #ifdef HAVE_KFILEMETADATA #include "baloowrap.h" #endif namespace Digikam { class ImageScannerCommit { public: enum Operation { NoOp, AddItem, UpdateItem }; public: ImageScannerCommit() : operation(NoOp), copyImageAttributesId(-1), commitImageInformation(false), commitImageMetadata(false), commitVideoMetadata(false), commitImagePosition(false), commitImageComments(false), commitImageCopyright(false), commitFaces(false), commitIPTCCore(false), hasColorTag(false), hasPickTag(false) { } public: Operation operation; qlonglong copyImageAttributesId; bool commitImageInformation; bool commitImageMetadata; bool commitVideoMetadata; bool commitImagePosition; bool commitImageComments; bool commitImageCopyright; bool commitFaces; bool commitIPTCCore; bool hasColorTag; bool hasPickTag; DatabaseFields::ImageInformation imageInformationFields; QVariantList imageInformationInfos; QVariantList imageMetadataInfos; QVariantList imagePositionInfos; CaptionsMap captions; QString headline; QString title; Template copyrightTemplate; QMap metadataFacesMap; QVariantList iptcCoreMetadataInfos; QList tagIds; QString historyXml; QString uuid; }; // --------------------------------------------------------------------------- class ImageScanner::Private { public: Private() : hasImage(false), hasMetadata(false), loadedFromDisk(false), scanMode(ModifiedScan), hasHistoryToResolve(false) { time.start(); } public: bool hasImage; bool hasMetadata; bool loadedFromDisk; QFileInfo fileInfo; DMetadata metadata; DImg img; ItemScanInfo scanInfo; ImageScanner::ScanMode scanMode; bool hasHistoryToResolve; ImageScannerCommit commit; QTime time; }; ImageScanner::ImageScanner(const QFileInfo& info, const ItemScanInfo& scanInfo) : d(new Private) { d->fileInfo = info; d->scanInfo = scanInfo; } ImageScanner::ImageScanner(const QFileInfo& info) : d(new Private) { d->fileInfo = info; } ImageScanner::ImageScanner(qlonglong imageid) : d(new Private) { ItemShortInfo shortInfo; { CoreDbAccess access; shortInfo = access.db()->getItemShortInfo(imageid); d->scanInfo = access.db()->getItemScanInfo(imageid); } QString albumRootPath = CollectionManager::instance()->albumRootPath(shortInfo.albumRootID); d->fileInfo = QFileInfo(CoreDbUrl::fromAlbumAndName(shortInfo.itemName, shortInfo.album, QUrl::fromLocalFile(albumRootPath), shortInfo.albumRootID).fileUrl().toLocalFile()); } ImageScanner::~ImageScanner() { qCDebug(DIGIKAM_DATABASE_LOG) << "Finishing took" << d->time.elapsed() << "ms"; delete d; } qlonglong ImageScanner::id() const { return d->scanInfo.id; } void ImageScanner::setCategory(DatabaseItem::Category category) { // we don't have the necessary information in this class, but in CollectionScanner d->scanInfo.category = category; } void ImageScanner::commit() { qCDebug(DIGIKAM_DATABASE_LOG) << "Scanning took" << d->time.restart() << "ms"; switch (d->commit.operation) { case ImageScannerCommit::NoOp: return; case ImageScannerCommit::AddItem: commitAddImage(); break; case ImageScannerCommit::UpdateItem: commitUpdateImage(); break; } if (d->commit.copyImageAttributesId != -1) { commitCopyImageAttributes(); return; } if (d->commit.commitImageInformation) { commitImageInformation(); } if (d->commit.commitImageMetadata) { commitImageMetadata(); } else if (d->commit.commitVideoMetadata) { commitVideoMetadata(); } if (d->commit.commitImagePosition) { commitImagePosition(); } if (d->commit.commitImageComments) { commitImageComments(); } if (d->commit.commitImageCopyright) { commitImageCopyright(); } if (d->commit.commitIPTCCore) { commitIPTCCore(); } if (!d->commit.tagIds.isEmpty()) { commitTags(); } if (d->commit.commitFaces) { commitFaces(); } commitImageHistory(); } void ImageScanner::fileModified() { loadFromDisk(); prepareUpdateImage(); scanFile(ModifiedScan); } void ImageScanner::newFile(int albumId) { loadFromDisk(); prepareAddImage(albumId); if (!scanFromIdenticalFile()) { scanFile(NewScan); } } void ImageScanner::newFileFullScan(int albumId) { loadFromDisk(); prepareAddImage(albumId); scanFile(NewScan); } void ImageScanner::rescan() { loadFromDisk(); prepareUpdateImage(); scanFile(Rescan); } void ImageScanner::copiedFrom(int albumId, qlonglong srcId) { loadFromDisk(); prepareAddImage(albumId); // first use source, if it exists if (!copyFromSource(srcId)) { // check if we can establish identity if (!scanFromIdenticalFile()) { // scan newly scanFile(NewScan); } } } const ItemScanInfo& ImageScanner::itemScanInfo() const { return d->scanInfo; } bool ImageScanner::hasHistoryToResolve() const { return d->hasHistoryToResolve; } bool lessThanForIdentity(const ItemScanInfo& a, const ItemScanInfo& b) { if (a.status != b.status) { // First: sort by status // put UndefinedStatus to back if (a.status == DatabaseItem::UndefinedStatus) { return false; } // enum values are in the order we want it return a.status < b.status; } else { // Second: sort by modification date, descending return a.modificationDate > b.modificationDate; } } bool ImageScanner::scanFromIdenticalFile() { // Get a list of other images that are identical. Source image shall not be included. // When using the Commit functionality, d->scanInfo.id can be null. QList candidates = CoreDbAccess().db()->getIdenticalFiles(d->scanInfo.uniqueHash, d->scanInfo.fileSize, d->scanInfo.id); if (!candidates.isEmpty()) { // Sort by priority, as implemented by custom lessThan() std::stable_sort(candidates.begin(), candidates.end(), lessThanForIdentity); qCDebug(DIGIKAM_DATABASE_LOG) << "Recognized" << d->fileInfo.filePath() << "as identical to item" << candidates.first().id; // Copy attributes. // Todo for the future is to worry about syncing identical files. d->commit.copyImageAttributesId = candidates.first().id; return true; } return false; } void ImageScanner::commitCopyImageAttributes() { CoreDbAccess().db()->copyImageAttributes(d->commit.copyImageAttributesId, d->scanInfo.id); // Also copy the similarity information SimilarityDbAccess().db()->copySimilarityAttributes(d->commit.copyImageAttributesId, d->scanInfo.id); // Remove grouping for copied or identical images. CoreDbAccess().db()->removeAllImageRelationsFrom(d->scanInfo.id, DatabaseRelation::Grouped); CoreDbAccess().db()->removeAllImageRelationsTo(d->scanInfo.id, DatabaseRelation::Grouped); } bool ImageScanner::copyFromSource(qlonglong srcId) { CoreDbAccess access; // some basic validity checking if (srcId == d->scanInfo.id) { return false; } ItemScanInfo info = access.db()->getItemScanInfo(srcId); if (!info.id) { return false; } qCDebug(DIGIKAM_DATABASE_LOG) << "Recognized" << d->fileInfo.filePath() << "as copied from" << srcId; d->commit.copyImageAttributesId = srcId; return true; } void ImageScanner::prepareAddImage(int albumId) { d->scanInfo.albumID = albumId; d->scanInfo.status = DatabaseItem::Visible; qCDebug(DIGIKAM_DATABASE_LOG) << "Adding new item" << d->fileInfo.filePath(); d->commit.operation = ImageScannerCommit::AddItem; } void ImageScanner::commitAddImage() { // get the image id of a deleted image info if existent and mark it as valid. // otherwise, create a new item. qlonglong imageId = CoreDbAccess().db()->getImageId(-1, d->scanInfo.itemName, DatabaseItem::Status::Trashed, d->scanInfo.category, d->scanInfo.modificationDate, d->scanInfo.fileSize, d->scanInfo.uniqueHash); if (imageId != -1) { qCDebug(DIGIKAM_DATABASE_LOG) << "Detected identical image info with id" << imageId << "and album id NULL of a removed image for image" << d->scanInfo.itemName; qCDebug(DIGIKAM_DATABASE_LOG) << "Will reuse this image info and set the status to visible and the album id to" << d->scanInfo.albumID; CoreDbAccess().db()->setItemAlbum(imageId,d->scanInfo.albumID); CoreDbAccess().db()->setItemStatus(imageId, DatabaseItem::Status::Visible); } else { d->scanInfo.id = CoreDbAccess().db()->addItem(d->scanInfo.albumID, d->scanInfo.itemName, d->scanInfo.status, d->scanInfo.category, d->scanInfo.modificationDate, d->scanInfo.fileSize, d->scanInfo.uniqueHash); } } void ImageScanner::prepareUpdateImage() { d->commit.operation = ImageScannerCommit::UpdateItem; } void ImageScanner::commitUpdateImage() { CoreDbAccess().db()->updateItem(d->scanInfo.id, d->scanInfo.category, d->scanInfo.modificationDate, d->scanInfo.fileSize, d->scanInfo.uniqueHash); } void ImageScanner::scanFile(ScanMode mode) { d->scanMode = mode; if (d->scanMode == ModifiedScan) { if (d->scanInfo.category == DatabaseItem::Image) { scanImageInformation(); scanImageHistoryIfModified(); } else if (d->scanInfo.category == DatabaseItem::Video) { scanVideoInformation(); // NOTE: Here, we only scan fields which can be expected to have changed, when we detect a change of file data. // It seems to me that at the moment video metadata contains such fields (which may change after editing). // In contrast, with photos, ImageMetadata contains fields which describe the moment of taking the photo, // which means they dont change. if (d->hasMetadata) { scanVideoMetadata(); } } } else { if (d->scanInfo.category == DatabaseItem::Image) { scanImageInformation(); if (d->hasMetadata) { scanImageMetadata(); scanImagePosition(); scanImageComments(); scanImageCopyright(); scanIPTCCore(); scanTags(); scanFaces(); scanImageHistory(); scanBalooInfo(); } } else if (d->scanInfo.category == DatabaseItem::Video) { scanVideoInformation(); if (d->hasMetadata) { scanVideoMetadata(); scanImagePosition(); scanImageComments(); scanImageCopyright(); scanIPTCCore(); scanTags(); } } else if (d->scanInfo.category == DatabaseItem::Audio) { scanAudioFile(); } else if (d->scanInfo.category == DatabaseItem::Other) { // unsupported } } } void ImageScanner::checkCreationDateFromMetadata(QVariant& dateFromMetadata) const { // creation date: fall back to file system property if (dateFromMetadata.isNull() || !dateFromMetadata.toDateTime().isValid()) { dateFromMetadata = creationDateFromFilesystem(d->fileInfo); } } bool ImageScanner::checkRatingFromMetadata(const QVariant& ratingFromMetadata) const { // should only be overwritten if set in metadata if (d->scanMode == Rescan) { if (ratingFromMetadata.isNull() || ratingFromMetadata.toInt() == -1) { return false; } } return true; } void ImageScanner::scanImageInformation() { d->commit.commitImageInformation = true; if (d->scanMode == NewScan || d->scanMode == Rescan) { d->commit.imageInformationFields = DatabaseFields::ImageInformationAll; MetadataFields fields; fields << MetadataInfo::Rating << MetadataInfo::CreationDate << MetadataInfo::DigitizationDate << MetadataInfo::Orientation; QVariantList metadataInfos = d->metadata.getMetadataFields(fields); checkCreationDateFromMetadata(metadataInfos[1]); if (!checkRatingFromMetadata(metadataInfos.at(0))) { d->commit.imageInformationFields &= ~DatabaseFields::Rating; metadataInfos.removeAt(0); } d->commit.imageInformationInfos = metadataInfos; } else { // Does _not_ update rating and orientation (unless dims were exchanged)! /* int orientation = d->metadata.getImageOrientation(); QVariantList data = CoreDbAccess().db()->getImageInformation(d->scanInfo.id, DatabaseFields::Width | DatabaseFields::Height | DatabaseFields::Orientation); if (data.size() == 3 && data[2].isValid() && data[2].toInt() != orientation) { // be careful not to overwrite our value set in the database // But there is a special case: if the dims were } */ d->commit.imageInformationFields = DatabaseFields::Width | DatabaseFields::Height | DatabaseFields::Format | DatabaseFields::ColorDepth | DatabaseFields::ColorModel; } QSize size = d->img.size(); d->commit.imageInformationInfos << size.width() << size.height() << detectImageFormat() << d->img.originalBitDepth() << d->img.originalColorModel(); } void ImageScanner::commitImageInformation() { if (d->scanMode == NewScan) { CoreDbAccess().db()->addImageInformation(d->scanInfo.id, d->commit.imageInformationInfos, d->commit.imageInformationFields); } else // d->scanMode == Rescan or d->scanMode == ModifiedScan { CoreDbAccess().db()->changeImageInformation(d->scanInfo.id, d->commit.imageInformationInfos, d->commit.imageInformationFields); } } static bool hasValidField(const QVariantList& list) { for (QVariantList::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) { if (!(*it).isNull()) { return true; } } return false; } static MetadataFields allImageMetadataFields() { // This list must reflect the order required by CoreDB::addImageMetadata MetadataFields fields; fields << MetadataInfo::Make << MetadataInfo::Model << MetadataInfo::Lens << MetadataInfo::Aperture << MetadataInfo::FocalLength << MetadataInfo::FocalLengthIn35mm << MetadataInfo::ExposureTime << MetadataInfo::ExposureProgram << MetadataInfo::ExposureMode << MetadataInfo::Sensitivity << MetadataInfo::FlashMode << MetadataInfo::WhiteBalance << MetadataInfo::WhiteBalanceColorTemperature << MetadataInfo::MeteringMode << MetadataInfo::SubjectDistance << MetadataInfo::SubjectDistanceCategory; return fields; } void ImageScanner::scanImageMetadata() { QVariantList metadataInfos = d->metadata.getMetadataFields(allImageMetadataFields()); if (hasValidField(metadataInfos)) { d->commit.commitImageMetadata = true; d->commit.imageMetadataInfos = metadataInfos; } } void ImageScanner::commitImageMetadata() { CoreDbAccess().db()->addImageMetadata(d->scanInfo.id, d->commit.imageMetadataInfos); } void ImageScanner::scanImagePosition() { // This list must reflect the order required by CoreDB::addImagePosition MetadataFields fields; fields << MetadataInfo::Latitude << MetadataInfo::LatitudeNumber << MetadataInfo::Longitude << MetadataInfo::LongitudeNumber << MetadataInfo::Altitude << MetadataInfo::PositionOrientation << MetadataInfo::PositionTilt << MetadataInfo::PositionRoll << MetadataInfo::PositionAccuracy << MetadataInfo::PositionDescription; QVariantList metadataInfos = d->metadata.getMetadataFields(fields); if (hasValidField(metadataInfos)) { d->commit.commitImagePosition = true; d->commit.imagePositionInfos = metadataInfos; } } void ImageScanner::commitImagePosition() { CoreDbAccess().db()->addImagePosition(d->scanInfo.id, d->commit.imagePositionInfos); } void ImageScanner::scanImageComments() { MetadataFields fields; fields << MetadataInfo::Headline << MetadataInfo::Title; QVariantList metadataInfos = d->metadata.getMetadataFields(fields); // handles all possible fields, multi-language, author, date CaptionsMap captions = d->metadata.getImageComments(); if (captions.isEmpty() && !hasValidField(metadataInfos)) { return; } d->commit.commitImageComments = true; d->commit.captions = captions; // Headline if (!metadataInfos.at(0).isNull()) { d->commit.headline = metadataInfos.at(0).toString(); } // Title if (!metadataInfos.at(1).isNull()) { d->commit.title = metadataInfos.at(1).toMap()[QLatin1String("x-default")].toString(); } } void ImageScanner::commitImageComments() { CoreDbAccess access; ImageComments comments(access, d->scanInfo.id); // Description if (!d->commit.captions.isEmpty()) { comments.replaceComments(d->commit.captions); } // Headline if (!d->commit.headline.isNull()) { comments.addHeadline(d->commit.headline); } // Title if (!d->commit.title.isNull()) { comments.addTitle(d->commit.title); } } void ImageScanner::scanImageCopyright() { Template t; if (!d->metadata.getCopyrightInformation(t)) { return; } d->commit.commitImageCopyright = true; d->commit.copyrightTemplate = t; } void ImageScanner::commitImageCopyright() { ImageCopyright copyright(d->scanInfo.id); // It is not clear if removeAll() should be called if d->scanMode == Rescan copyright.removeAll(); copyright.setFromTemplate(d->commit.copyrightTemplate); } void ImageScanner::scanIPTCCore() { MetadataFields fields; fields << MetadataInfo::IptcCoreLocationInfo << MetadataInfo::IptcCoreIntellectualGenre << MetadataInfo::IptcCoreJobID << MetadataInfo::IptcCoreScene << MetadataInfo::IptcCoreSubjectCode; QVariantList metadataInfos = d->metadata.getMetadataFields(fields); if (!hasValidField(metadataInfos)) { return; } d->commit.commitIPTCCore = true; d->commit.iptcCoreMetadataInfos = metadataInfos; } void ImageScanner::commitIPTCCore() { ImageExtendedProperties props(d->scanInfo.id); if (!d->commit.iptcCoreMetadataInfos.at(0).isNull()) { IptcCoreLocationInfo loc = d->commit.iptcCoreMetadataInfos.at(0).value(); if (!loc.isNull()) { props.setLocation(loc); } } if (!d->commit.iptcCoreMetadataInfos.at(1).isNull()) { props.setIntellectualGenre(d->commit.iptcCoreMetadataInfos.at(1).toString()); } if (!d->commit.iptcCoreMetadataInfos.at(2).isNull()) { props.setJobId(d->commit.iptcCoreMetadataInfos.at(2).toString()); } if (!d->commit.iptcCoreMetadataInfos.at(3).isNull()) { props.setScene(d->commit.iptcCoreMetadataInfos.at(3).toStringList()); } if (!d->commit.iptcCoreMetadataInfos.at(4).isNull()) { props.setSubjectCode(d->commit.iptcCoreMetadataInfos.at(4).toStringList()); } } void ImageScanner::scanTags() { // Check Keywords tag paths. QVariant var = d->metadata.getMetadataField(MetadataInfo::Keywords); QStringList keywords = var.toStringList(); QStringList filteredKeywords; // Extra empty tags check, empty tag = root tag which is not asignable for (int index = 0 ; index < keywords.size() ; index++) { QString keyword = keywords.at(index); if (!keyword.isEmpty()) { // _Digikam_root_tag_ is present in some photos tagged with older // version of digiKam, must be removed if (keyword.contains(QRegExp(QLatin1String("(_Digikam_root_tag_/|/_Digikam_root_tag_|_Digikam_root_tag_)")))) { keyword = keyword.replace(QRegExp(QLatin1String("(_Digikam_root_tag_/|/_Digikam_root_tag_|_Digikam_root_tag_)")), QLatin1String("")); } filteredKeywords.append(keyword); } } if (!filteredKeywords.isEmpty()) { // get tag ids, create if necessary QList tagIds = TagsCache::instance()->getOrCreateTags(filteredKeywords); d->commit.tagIds += tagIds; } // Check Pick Label tag. int pickId = d->metadata.getImagePickLabel(); if (pickId != -1) { qCDebug(DIGIKAM_DATABASE_LOG) << "Pick Label found : " << pickId; int tagId = TagsCache::instance()->tagForPickLabel((PickLabel)pickId); if (tagId) { d->commit.tagIds << tagId; d->commit.hasPickTag = true; qCDebug(DIGIKAM_DATABASE_LOG) << "Assigned Pick Label Tag : " << tagId; } else { qCDebug(DIGIKAM_DATABASE_LOG) << "Cannot find Pick Label Tag for : " << pickId; } } // Check Color Label tag. int colorId = d->metadata.getImageColorLabel(); if (colorId != -1) { qCDebug(DIGIKAM_DATABASE_LOG) << "Color Label found : " << colorId; int tagId = TagsCache::instance()->tagForColorLabel((ColorLabel)colorId); if (tagId) { d->commit.tagIds << tagId; d->commit.hasColorTag = true; qCDebug(DIGIKAM_DATABASE_LOG) << "Assigned Color Label Tag : " << tagId; } else { qCDebug(DIGIKAM_DATABASE_LOG) << "Cannot find Color Label Tag for : " << colorId; } } } void ImageScanner::commitTags() { QList currentTags = CoreDbAccess().db()->getItemTagIDs(d->scanInfo.id); QVector colorTags = TagsCache::instance()->colorLabelTags(); QVector pickTags = TagsCache::instance()->pickLabelTags(); QList removeTags; foreach(int cTag, currentTags) { if ((d->commit.hasColorTag && colorTags.contains(cTag)) || (d->commit.hasPickTag && pickTags.contains(cTag))) { removeTags << cTag; } } if (!removeTags.isEmpty()) { CoreDbAccess().db()->removeTagsFromItems(QList() << d->scanInfo.id, removeTags); } CoreDbAccess().db()->addTagsToItems(QList() << d->scanInfo.id, d->commit.tagIds); } void ImageScanner::scanFaces() { QSize size = d->img.size(); if (!size.isValid()) { return; } QMultiMap metadataFacesMap; if (!d->metadata.getImageFacesMap(metadataFacesMap)) { return; } d->commit.commitFaces = true; d->commit.metadataFacesMap = metadataFacesMap; } void ImageScanner::commitFaces() { QSize size = d->img.size(); QMap::const_iterator it; for (it = d->commit.metadataFacesMap.constBegin() ; it != d->commit.metadataFacesMap.constEnd() ; ++it) { QString name = it.key(); QRectF rect = it.value().toRectF(); if (name.isEmpty() || !rect.isValid()) { continue; } int tagId = FaceTags::getOrCreateTagForPerson(name); if (!tagId) { qCDebug(DIGIKAM_DATABASE_LOG) << "Failed to create a person tag for name" << name; } TagRegion region(TagRegion::relativeToAbsolute(rect, size)); FaceTagsEditor editor; editor.add(d->scanInfo.id, tagId, region, false); } } void ImageScanner::scanImageHistory() { /** Stage 1 of history scanning */ d->commit.historyXml = d->metadata.getImageHistory(); d->commit.uuid = d->metadata.getImageUniqueId(); } void ImageScanner::commitImageHistory() { if (!d->commit.historyXml.isEmpty()) { CoreDbAccess().db()->setImageHistory(d->scanInfo.id, d->commit.historyXml); // Delay history resolution by setting this tag: // Resolution depends on the presence of other images, possibly only when the scanning process has finished CoreDbAccess().db()->addItemTag(d->scanInfo.id, TagsCache::instance()-> getOrCreateInternalTag(InternalTagName::needResolvingHistory())); d->hasHistoryToResolve = true; } if (!d->commit.uuid.isNull()) { CoreDbAccess().db()->setImageUuid(d->scanInfo.id, d->commit.uuid); } } void ImageScanner::scanImageHistoryIfModified() { // If a file has a modified history, it must have a new UUID QString previousUuid = CoreDbAccess().db()->getImageUuid(d->scanInfo.id); QString currentUuid = d->metadata.getImageUniqueId(); if (!currentUuid.isEmpty() && previousUuid != currentUuid) { scanImageHistory(); } } bool ImageScanner::resolveImageHistory(qlonglong id, QList* needTaggingIds) { ImageHistoryEntry history = CoreDbAccess().db()->getImageHistory(id); return resolveImageHistory(id, history.history, needTaggingIds); } bool ImageScanner::resolveImageHistory(qlonglong imageId, const QString& historyXml, QList* needTaggingIds) { /** Stage 2 of history scanning */ if (historyXml.isNull()) { return true; // "true" means nothing is left to resolve } DImageHistory history = DImageHistory::fromXml(historyXml); if (history.isNull()) { return true; } ImageHistoryGraph graph; graph.addScannedHistory(history, imageId); if (!graph.hasEdges()) { return true; } QPair, QList > cloud = graph.relationCloudParallel(); CoreDbAccess().db()->addImageRelations(cloud.first, cloud.second, DatabaseRelation::DerivedFrom); int needResolvingTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::needResolvingHistory()); int needTaggingTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::needTaggingHistoryGraph()); // remove the needResolvingHistory tag from all images in graph CoreDbAccess().db()->removeTagsFromItems(graph.allImageIds(), QList() << needResolvingTag); // mark a single image from the graph (sufficient for find the full relation cloud) QList roots = graph.rootImages(); if (!roots.isEmpty()) { CoreDbAccess().db()->addItemTag(roots.first().id(), needTaggingTag); if (needTaggingIds) { *needTaggingIds << roots.first().id(); } } return !graph.hasUnresolvedEntries(); } void ImageScanner::tagImageHistoryGraph(qlonglong id) { /** Stage 3 of history scanning */ ImageInfo info(id); if (info.isNull()) { return; } //qCDebug(DIGIKAM_DATABASE_LOG) << "tagImageHistoryGraph" << id; // Load relation cloud, history of info and of all leaves of the tree into the graph, fully resolved ImageHistoryGraph graph = ImageHistoryGraph::fromInfo(info, ImageHistoryGraph::LoadAll, ImageHistoryGraph::NoProcessing); qCDebug(DIGIKAM_DATABASE_LOG) << graph; int originalVersionTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::originalVersion()); int currentVersionTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::currentVersion()); int intermediateVersionTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::intermediateVersion()); int needTaggingTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::needTaggingHistoryGraph()); // Remove all relevant tags CoreDbAccess().db()->removeTagsFromItems(graph.allImageIds(), QList() << originalVersionTag << currentVersionTag << intermediateVersionTag << needTaggingTag); if (!graph.hasEdges()) { return; } // get category info QList originals, intermediates, currents; QHash types = graph.categorize(); QHash::const_iterator it; for (it = types.constBegin() ; it != types.constEnd() ; ++it) { qCDebug(DIGIKAM_DATABASE_LOG) << "Image" << it.key().id() << "type" << it.value(); HistoryImageId::Types types = it.value(); if (types & HistoryImageId::Original) { originals << it.key().id(); } if (types & HistoryImageId::Intermediate) { intermediates << it.key().id(); } if (types & HistoryImageId::Current) { currents << it.key().id(); } } if (!originals.isEmpty()) { CoreDbAccess().db()->addTagsToItems(originals, QList() << originalVersionTag); } if (!intermediates.isEmpty()) { CoreDbAccess().db()->addTagsToItems(intermediates, QList() << intermediateVersionTag); } if (!currents.isEmpty()) { CoreDbAccess().db()->addTagsToItems(currents, QList() << currentVersionTag); } } DImageHistory ImageScanner::resolvedImageHistory(const DImageHistory& history, bool mustBeAvailable) { DImageHistory h; foreach(const DImageHistory::Entry& e, history.entries()) { // Copy entry, without referredImages DImageHistory::Entry entry; entry.action = e.action; // resolve referredImages foreach(const HistoryImageId& id, e.referredImages) { QList imageIds = resolveHistoryImageId(id); // append each image found in collection to referredImages foreach(qlonglong imageId, imageIds) { ImageInfo info(imageId); if (info.isNull()) { continue; } if (mustBeAvailable) { CollectionLocation location = CollectionManager::instance()->locationForAlbumRootId(info.albumRootId()); if (!location.isAvailable()) { continue; } } HistoryImageId newId = info.historyImageId(); newId.setType(id.m_type); entry.referredImages << newId; } } // add to history h.entries() << entry; } return h; } bool ImageScanner::sameReferredImage(const HistoryImageId& id1, const HistoryImageId& id2) { if (!id1.isValid() || !id2.isValid()) { return false; } /* * We give the UUID the power of equivalence that none of the other criteria has: * For two images a,b with uuids x,y, where x and y not null, * a (same image as) b <=> x == y */ if (id1.hasUuid() && id2.hasUuid()) { return id1.m_uuid == id2.m_uuid; } if (id1.hasUniqueHashIdentifier() && id1.m_uniqueHash == id2.m_uniqueHash && id1.m_fileSize == id2.m_fileSize) { return true; } if (id1.hasFileName() && id1.hasCreationDate() && id1.m_fileName == id2.m_fileName && id1.m_creationDate == id2.m_creationDate) { return true; } if (id1.hasFileOnDisk() && id1.m_filePath == id2.m_filePath && id1.m_fileName == id2.m_fileName) { return true; } return false; } // Returns true if both have the same UUID, or at least one of the two has no UUID // Returns false iff both have a UUID and the UUIDs differ static bool uuidDoesNotDiffer(const HistoryImageId& referenceId, qlonglong id) { if (referenceId.hasUuid()) { QString uuid = CoreDbAccess().db()->getImageUuid(id); if (!uuid.isEmpty()) { return referenceId.m_uuid == uuid; } } return true; } static QList mergedIdLists(const HistoryImageId& referenceId, const QList& uuidList, const QList& candidates) { QList results; // uuidList are definite results results = uuidList; // Add a candidate if it has the same UUID, or either reference or candidate have a UUID // (other way round: do not add a candidate which positively has a different UUID) foreach(qlonglong candidate, candidates) { if (results.contains(candidate)) { continue; // already in list, skip } if (uuidDoesNotDiffer(referenceId, candidate)) { results << candidate; } } return results; } QList ImageScanner::resolveHistoryImageId(const HistoryImageId& historyId) { // first and foremost: UUID QList uuidList; if (historyId.hasUuid()) { uuidList = CoreDbAccess().db()->getItemsForUuid(historyId.m_uuid); // If all images had a UUID, we would be finished and could return here with a result: /* if (!uuidList.isEmpty()) { return uuidList; } */ // But as identical images may have no UUID yet, we need to continue } // Second: uniqueHash + fileSize. Sufficient to assume that a file is identical, but subject to frequent change. if (historyId.hasUniqueHashIdentifier() && CoreDbAccess().db()->isUniqueHashV2()) { QList infos = CoreDbAccess().db()->getIdenticalFiles(historyId.m_uniqueHash, historyId.m_fileSize); if (!infos.isEmpty()) { QList ids; foreach(const ItemScanInfo& info, infos) { if (info.status != DatabaseItem::Status::Trashed && info.status != DatabaseItem::Status::Obsolete) { ids << info.id; } } return mergedIdLists(historyId, uuidList, ids); } } // As a third combination, we try file name and creation date. Susceptible to renaming, // but not to metadata changes. if (historyId.hasFileName() && historyId.hasCreationDate()) { QList ids = CoreDbAccess().db()->findByNameAndCreationDate(historyId.m_fileName, historyId.m_creationDate); if (!ids.isEmpty()) { return mergedIdLists(historyId, uuidList, ids); } } // Another possibility: If the original UUID is given, we can find all relations for the image with this UUID, // and make an assumption from this group of images. Currently not implemented. // resolve old-style by full file path if (historyId.hasFileOnDisk()) { QFileInfo file(historyId.filePath()); if (file.exists()) { CollectionLocation location = CollectionManager::instance()->locationForPath(historyId.path()); if (!location.isNull()) { QString album = CollectionManager::instance()->album(file.path()); QString name = file.fileName(); ItemShortInfo info = CoreDbAccess().db()->getItemShortInfo(location.id(), album, name); if (info.id) { return mergedIdLists(historyId, uuidList, QList() << info.id); } } } } return uuidList; } // --------------------------------------------------------------------------------------- class lessThanByProximityToSubject { public: explicit lessThanByProximityToSubject(const ImageInfo& subject) : subject(subject) { } bool operator()(const ImageInfo& a, const ImageInfo& b) { if (a.isNull() || b.isNull()) { // both null: false // only a null: a greater than b (null infos at end of list) // (a && b) || (a && !b) = a // only b null: a less than b if (a.isNull()) { return false; } return true; } if (a == b) { return false; } // same collection if (a.albumId() != b.albumId()) { // same album if (a.albumId() == subject.albumId()) { return true; } if (b.albumId() == subject.albumId()) { return false; } if (a.albumRootId() != b.albumRootId()) { // different collection if (a.albumRootId() == subject.albumRootId()) { return true; } if (b.albumRootId() == subject.albumRootId()) { return false; } } } if (a.modDateTime() != b.modDateTime()) { return a.modDateTime() < b.modDateTime(); } if (a.name() != b.name()) { return qAbs(a.name().compare(subject.name())) < qAbs(b.name().compare(subject.name())); } // last resort return (a.id() < b.id()); } public: ImageInfo subject; }; void ImageScanner::sortByProximity(QList& list, const ImageInfo& subject) { if (!list.isEmpty() && !subject.isNull()) { std::stable_sort(list.begin(), list.end(), lessThanByProximityToSubject(subject)); } } // --------------------------------------------------------------------------------------- static MetadataFields allVideoMetadataFields() { // This list must reflect the order required by CoreDB::addVideoMetadata MetadataFields fields; fields << MetadataInfo::AspectRatio << MetadataInfo::AudioBitRate << MetadataInfo::AudioChannelType << MetadataInfo::AudioCodec << MetadataInfo::Duration << MetadataInfo::FrameRate << MetadataInfo::VideoCodec; return fields; } void ImageScanner::scanVideoInformation() { d->commit.commitImageInformation = true; if (d->scanMode == NewScan || d->scanMode == Rescan) { MetadataFields fields; fields << MetadataInfo::Rating << MetadataInfo::CreationDate << MetadataInfo::DigitizationDate << MetadataInfo::Orientation; QVariantList metadataInfos = d->metadata.getMetadataFields(fields); d->commit.imageInformationFields = DatabaseFields::Rating | DatabaseFields::CreationDate | DatabaseFields::DigitizationDate | DatabaseFields::Orientation; checkCreationDateFromMetadata(metadataInfos[1]); if (!checkRatingFromMetadata(metadataInfos.at(0))) { d->commit.imageInformationFields &= ~DatabaseFields::Rating; metadataInfos.removeAt(0); } d->commit.imageInformationInfos = metadataInfos; } d->commit.imageInformationInfos << d->metadata.getMetadataField(MetadataInfo::VideoWidth) << d->metadata.getMetadataField(MetadataInfo::VideoHeight); d->commit.imageInformationFields |= DatabaseFields::Width | DatabaseFields::Height; // TODO: Please check / improve / rewrite detectVideoFormat(). // The format strings shall be uppercase, and a clearly defined set // (all format strings used in the database should be defined in advance) d->commit.imageInformationInfos << detectVideoFormat(); d->commit.imageInformationFields |= DatabaseFields::Format; - // There is use of bit depth, but not ColorModel - // For bit depth - 8bit, 16bit with videos - d->commit.imageInformationInfos << d->metadata.getMetadataField(MetadataInfo::VideoBitDepth); + d->commit.imageInformationInfos << d->metadata.getMetadataField(MetadataInfo::VideoBitDepth); d->commit.imageInformationFields |= DatabaseFields::ColorDepth; + + d->commit.imageInformationInfos << d->metadata.getMetadataField(MetadataInfo::VideoColorSpace); + d->commit.imageInformationFields |= DatabaseFields::ColorModel; } // commitImageInformation method is reused void ImageScanner::scanVideoMetadata() { QVariantList metadataInfos = d->metadata.getMetadataFields(allVideoMetadataFields()); if (hasValidField(metadataInfos)) { d->commit.commitVideoMetadata = true; // reuse imageMetadataInfos field d->commit.imageMetadataInfos = metadataInfos; } } void ImageScanner::commitVideoMetadata() { CoreDbAccess().db()->addVideoMetadata(d->scanInfo.id, d->commit.imageMetadataInfos); } // --------------------------------------------------------------------------------------- void ImageScanner::scanAudioFile() { /** * @todo */ d->commit.commitImageInformation = true; d->commit.imageInformationInfos << -1 << creationDateFromFilesystem(d->fileInfo) << detectAudioFormat(); d->commit.imageInformationFields = DatabaseFields::Rating | DatabaseFields::CreationDate | DatabaseFields::Format; } void ImageScanner::loadFromDisk() { if (d->loadedFromDisk) { return; } d->loadedFromDisk = true; d->metadata.registerMetadataSettings(); d->hasMetadata = d->metadata.load(d->fileInfo.filePath()); if (d->scanInfo.category == DatabaseItem::Image) { d->hasImage = d->img.loadImageInfo(d->fileInfo.filePath(), false, false, false, false); } else { d->hasImage = false; } d->scanInfo.itemName = d->fileInfo.fileName(); d->scanInfo.modificationDate = d->fileInfo.lastModified(); d->scanInfo.fileSize = d->fileInfo.size(); // category is set by setCategory // NOTE: call uniqueHash after loading the image above, else it will fail d->scanInfo.uniqueHash = uniqueHash(); // faster than loading twice from disk if (d->hasMetadata) { d->img.setMetadata(d->metadata.data()); } } QString ImageScanner::uniqueHash() const { // the QByteArray is an ASCII hex string if (d->scanInfo.category == DatabaseItem::Image) { if (CoreDbAccess().db()->isUniqueHashV2()) return QString::fromUtf8(d->img.getUniqueHashV2()); else return QString::fromUtf8(d->img.getUniqueHash()); } else { if (CoreDbAccess().db()->isUniqueHashV2()) return QString::fromUtf8(DImg::getUniqueHashV2(d->fileInfo.filePath())); else return QString::fromUtf8(DImg::getUniqueHash(d->fileInfo.filePath())); } } QString ImageScanner::detectImageFormat() const { DImg::FORMAT dimgFormat = d->img.detectedFormat(); switch (dimgFormat) { case DImg::JPEG: return QLatin1String("JPG"); case DImg::PNG: return QLatin1String("PNG"); case DImg::TIFF: return QLatin1String("TIFF"); case DImg::PPM: return QLatin1String("PPM"); case DImg::JP2K: return QLatin1String("JP2"); case DImg::PGF: return QLatin1String("PGF"); case DImg::RAW: { QString format = QLatin1String("RAW-"); format += d->fileInfo.suffix().toUpper(); return format; } case DImg::NONE: case DImg::QIMAGE: { QByteArray format = QImageReader::imageFormat(d->fileInfo.filePath()); if (!format.isEmpty()) { return QString::fromUtf8(format).toUpper(); } break; } } // See BUG #339341: Take file name suffix instead type mime analyze. return d->fileInfo.suffix().toUpper(); } QString ImageScanner::detectVideoFormat() const { QString suffix = d->fileInfo.suffix().toUpper(); if (suffix == QLatin1String("MPEG") || suffix == QLatin1String("MPG") || suffix == QLatin1String("MPO") || suffix == QLatin1String("MPE")) { return QLatin1String("MPEG"); } if (suffix == QLatin1String("ASF") || suffix == QLatin1String("WMV")) { return QLatin1String("WMV"); } if (suffix == QLatin1String("AVI") || suffix == QLatin1String("DIVX") ) { return QLatin1String("AVI"); } if (suffix == QLatin1String("MKV") || suffix == QLatin1String("MKS")) { return QLatin1String("MKV"); } if (suffix == QLatin1String("M4V") || suffix == QLatin1String("MOV") || suffix == QLatin1String("M2V") ) { return QLatin1String("MOV"); } if (suffix == QLatin1String("3GP") || suffix == QLatin1String("3G2") ) { return QLatin1String("3GP"); } return suffix; } QString ImageScanner::detectAudioFormat() const { return d->fileInfo.suffix().toUpper(); } QDateTime ImageScanner::creationDateFromFilesystem(const QFileInfo& info) { // creation date is not what it seems on Unix QDateTime ctime = info.created(); QDateTime mtime = info.lastModified(); if (ctime.isNull()) { return mtime; } if (mtime.isNull()) { return ctime; } return qMin(ctime, mtime); } QString ImageScanner::formatToString(const QString& format) { // image ------------------------------------------------------------------- if (format == QLatin1String("JPG")) { return QLatin1String("JPEG"); } else if (format == QLatin1String("PNG")) { return format; } else if (format == QLatin1String("TIFF")) { return format; } else if (format == QLatin1String("PPM")) { return format; } else if (format == QLatin1String("JP2") || format == QLatin1String("JP2k") || format == QLatin1String("JP2K")) { return QLatin1String("JPEG 2000"); } else if (format.startsWith(QLatin1String("RAW-"))) { return i18nc("RAW image file (), the parentheses contain the file suffix, like MRW", "RAW image file (%1)", format.mid(4)); } // video ------------------------------------------------------------------- else if (format == QLatin1String("MPEG")) { return format; } else if (format == QLatin1String("AVI")) { return format; } else if (format == QLatin1String("MOV")) { return QLatin1String("Quicktime"); } else if (format == QLatin1String("WMF")) { return QLatin1String("Windows MetaFile"); } else if (format == QLatin1String("WMV")) { return QLatin1String("Windows Media Video"); } else if (format == QLatin1String("MP4")) { return QLatin1String("MPEG-4"); } else if (format == QLatin1String("3GP")) { return QLatin1String("3GPP"); } // audio ------------------------------------------------------------------- else if (format == QLatin1String("OGG")) { return QLatin1String("Ogg"); } else if (format == QLatin1String("MP3")) { return format; } else if (format == QLatin1String("WMA")) { return QLatin1String("Windows Media Audio"); } else if (format == QLatin1String("WAV")) { return QLatin1String("WAVE"); } else { return format; } } QString ImageScanner::iptcCorePropertyName(MetadataInfo::Field field) { // These strings are specified in DBSCHEMA.ods switch (field) { // copyright table case MetadataInfo::IptcCoreCopyrightNotice: return QLatin1String("copyrightNotice"); case MetadataInfo::IptcCoreCreator: return QLatin1String("creator"); case MetadataInfo::IptcCoreProvider: return QLatin1String("provider"); case MetadataInfo::IptcCoreRightsUsageTerms: return QLatin1String("rightsUsageTerms"); case MetadataInfo::IptcCoreSource: return QLatin1String("source"); case MetadataInfo::IptcCoreCreatorJobTitle: return QLatin1String("creatorJobTitle"); case MetadataInfo::IptcCoreInstructions: return QLatin1String("instructions"); // ImageProperties table case MetadataInfo::IptcCoreCountryCode: return QLatin1String("countryCode"); case MetadataInfo::IptcCoreCountry: return QLatin1String("country"); case MetadataInfo::IptcCoreCity: return QLatin1String("city"); case MetadataInfo::IptcCoreLocation: return QLatin1String("location"); case MetadataInfo::IptcCoreProvinceState: return QLatin1String("provinceState"); case MetadataInfo::IptcCoreIntellectualGenre: return QLatin1String("intellectualGenre"); case MetadataInfo::IptcCoreJobID: return QLatin1String("jobId"); case MetadataInfo::IptcCoreScene: return QLatin1String("scene"); case MetadataInfo::IptcCoreSubjectCode: return QLatin1String("subjectCode"); case MetadataInfo::IptcCoreContactInfoCity: return QLatin1String("creatorContactInfo.city"); case MetadataInfo::IptcCoreContactInfoCountry: return QLatin1String("creatorContactInfo.country"); case MetadataInfo::IptcCoreContactInfoAddress: return QLatin1String("creatorContactInfo.address"); case MetadataInfo::IptcCoreContactInfoPostalCode: return QLatin1String("creatorContactInfo.postalCode"); case MetadataInfo::IptcCoreContactInfoProvinceState: return QLatin1String("creatorContactInfo.provinceState"); case MetadataInfo::IptcCoreContactInfoEmail: return QLatin1String("creatorContactInfo.email"); case MetadataInfo::IptcCoreContactInfoPhone: return QLatin1String("creatorContactInfo.phone"); case MetadataInfo::IptcCoreContactInfoWebUrl: return QLatin1String("creatorContactInfo.webUrl"); default: return QString(); } } void ImageScanner::scanBalooInfo() { #ifdef HAVE_KFILEMETADATA BalooWrap* const baloo = BalooWrap::instance(); if (!baloo->getSyncToDigikam()) { return; } BalooInfo bInfo = baloo->getSemanticInfo(QUrl::fromLocalFile(d->fileInfo.absoluteFilePath())); if (!bInfo.tags.isEmpty()) { // get tag ids, create if necessary QList tagIds = TagsCache::instance()->getOrCreateTags(bInfo.tags); d->commit.tagIds += tagIds; } if (bInfo.rating != -1) { if (!d->commit.imageInformationFields.testFlag(DatabaseFields::Rating)) { d->commit.imageInformationFields |= DatabaseFields::Rating; d->commit.imageInformationInfos.insert(0, QVariant(bInfo.rating)); } } if (!bInfo.comment.isEmpty()) { qCDebug(DIGIKAM_DATABASE_LOG) << "+++++++++++++++++++++Comment " << bInfo.comment; if (!d->commit.captions.contains(QLatin1String("x-default"))) { CaptionValues val; val.caption = bInfo.comment; d->commit.commitImageComments = true; d->commit.captions.insert(QLatin1String("x-default"), val); } } #endif // HAVE_KFILEMETADATA } void ImageScanner::fillCommonContainer(qlonglong imageid, ImageCommonContainer* const container) { QVariantList imagesFields; QVariantList imageInformationFields; { CoreDbAccess access; imagesFields = access.db()->getImagesFields(imageid, DatabaseFields::Name | + DatabaseFields::Category | DatabaseFields::ModificationDate | DatabaseFields::FileSize); imageInformationFields = access.db()->getImageInformation(imageid, DatabaseFields::Rating | DatabaseFields::CreationDate | DatabaseFields::DigitizationDate | DatabaseFields::Orientation | DatabaseFields::Width | DatabaseFields::Height | DatabaseFields::Format | DatabaseFields::ColorDepth | DatabaseFields::ColorModel); } if (!imagesFields.isEmpty()) { container->fileName = imagesFields.at(0).toString(); - container->fileModificationDate = imagesFields.at(1).toDateTime(); - container->fileSize = imagesFields.at(2).toLongLong(); + container->fileModificationDate = imagesFields.at(2).toDateTime(); + container->fileSize = imagesFields.at(3).toLongLong(); } if (!imageInformationFields.isEmpty()) { container->rating = imageInformationFields.at(0).toInt(); container->creationDate = imageInformationFields.at(1).toDateTime(); container->digitizationDate = imageInformationFields.at(2).toDateTime(); container->orientation = DMetadata::valueToString(imageInformationFields.at(3), MetadataInfo::Orientation); container->width = imageInformationFields.at(4).toInt(); container->height = imageInformationFields.at(5).toInt(); container->format = formatToString(imageInformationFields.at(6).toString()); container->colorDepth = imageInformationFields.at(7).toInt(); - container->colorModel = DImg::colorModelToString((DImg::COLORMODEL)imageInformationFields.at(8).toInt()); + + container->colorModel + = (imagesFields.at(1).toInt() == DatabaseItem::Video) ? + DMetadata::videoColorModelToString(imageInformationFields.at(8).toInt()) : + DImg::colorModelToString((DImg::COLORMODEL)imageInformationFields.at(8).toInt()); } } void ImageScanner::fillMetadataContainer(qlonglong imageid, ImageMetadataContainer* const container) { // read from database QVariantList fields = CoreDbAccess().db()->getImageMetadata(imageid); // check we have at least one valid field container->allFieldsNull = !hasValidField(fields); if (container->allFieldsNull) { return; } // DMetadata does all translation work QStringList strings = DMetadata::valuesToString(fields, allImageMetadataFields()); // associate with hard-coded variables container->make = strings.at(0); container->model = strings.at(1); container->lens = strings.at(2); container->aperture = strings.at(3); container->focalLength = strings.at(4); container->focalLength35 = strings.at(5); container->exposureTime = strings.at(6); container->exposureProgram = strings.at(7); container->exposureMode = strings.at(8); container->sensitivity = strings.at(9); container->flashMode = strings.at(10); container->whiteBalance = strings.at(11); container->whiteBalanceColorTemperature = strings.at(12); container->meteringMode = strings.at(13); container->subjectDistance = strings.at(14); container->subjectDistanceCategory = strings.at(15); } void ImageScanner::fillVideoMetadataContainer(qlonglong imageid, VideoMetadataContainer* const container) { // read from database QVariantList fields = CoreDbAccess().db()->getVideoMetadata(imageid); // check we have at least one valid field container->allFieldsNull = !hasValidField(fields); if (container->allFieldsNull) { return; } // DMetadata does all translation work QStringList strings = DMetadata::valuesToString(fields, allVideoMetadataFields()); // associate with hard-coded variables container->aspectRatio = strings.at(0); container->audioBitRate = strings.at(1); container->audioChannelType = strings.at(2); container->audioCodec = strings.at(3); container->duration = strings.at(4); container->frameRate = strings.at(5); container->videoCodec = strings.at(6); } } // namespace Digikam diff --git a/libs/dmetadata/dmetadata.cpp b/libs/dmetadata/dmetadata.cpp index ab291c6f3e..7d763a89d4 100644 --- a/libs/dmetadata/dmetadata.cpp +++ b/libs/dmetadata/dmetadata.cpp @@ -1,3409 +1,3411 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2006-02-23 * Description : image metadata interface * * Copyright (C) 2006-2018 by Gilles Caulier * Copyright (C) 2006-2013 by Marcel Wiesweg * Copyright (C) 2011 by Leif Huhn * * 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. * * ============================================================ */ #include "dmetadata.h" // C++ includes #include // Qt includes #include #include #include #include // Local includes #include "rawinfo.h" #include "drawdecoder.h" #include "filereadwritelock.h" #include "metadatasettings.h" #include "template.h" #include "dimg.h" #include "digikam_version.h" #include "digikam_globals.h" #include "digikam_debug.h" namespace Digikam { DMetadata::DMetadata() : MetaEngine() { registerMetadataSettings(); } DMetadata::DMetadata(const QString& filePath) : MetaEngine() { registerMetadataSettings(); load(filePath); } DMetadata::DMetadata(const MetaEngineData& data) : MetaEngine(data) { registerMetadataSettings(); } DMetadata::~DMetadata() { } void DMetadata::registerMetadataSettings() { setSettings(MetadataSettings::instance()->settings()); } void DMetadata::setSettings(const MetadataSettingsContainer& settings) { setUseXMPSidecar4Reading(settings.useXMPSidecar4Reading); setWriteRawFiles(settings.writeRawFiles); setMetadataWritingMode(settings.metadataWritingMode); setUpdateFileTimeStamp(settings.updateFileTimeStamp); } bool DMetadata::load(const QString& filePath) { // In first, we trying to get metadata using Exiv2, // else we will use other engine to extract minimal information. FileReadLocker lock(filePath); if (!MetaEngine::load(filePath)) { if (!loadUsingRawEngine(filePath)) { if (!loadUsingFFmpeg(filePath)) { return false; } } } return true; } bool DMetadata::save(const QString& filePath) const { FileWriteLocker lock(filePath); return MetaEngine::save(filePath); } bool DMetadata::applyChanges() const { FileWriteLocker lock(getFilePath()); return MetaEngine::applyChanges(); } bool DMetadata::loadUsingRawEngine(const QString& filePath) { RawInfo identify; if (DRawDecoder::rawFileIdentify(identify, filePath)) { long int num=1, den=1; if (!identify.model.isNull()) { setExifTagString("Exif.Image.Model", identify.model); } if (!identify.make.isNull()) { setExifTagString("Exif.Image.Make", identify.make); } if (!identify.owner.isNull()) { setExifTagString("Exif.Image.Artist", identify.owner); } if (identify.sensitivity != -1) { setExifTagLong("Exif.Photo.ISOSpeedRatings", lroundf(identify.sensitivity)); } if (identify.dateTime.isValid()) { setImageDateTime(identify.dateTime, false); } if (identify.exposureTime != -1.0) { convertToRationalSmallDenominator(identify.exposureTime, &num, &den); setExifTagRational("Exif.Photo.ExposureTime", num, den); } if (identify.aperture != -1.0) { convertToRational(identify.aperture, &num, &den, 8); setExifTagRational("Exif.Photo.ApertureValue", num, den); } if (identify.focalLength != -1.0) { convertToRational(identify.focalLength, &num, &den, 8); setExifTagRational("Exif.Photo.FocalLength", num, den); } if (identify.imageSize.isValid()) { setImageDimensions(identify.imageSize); } // A RAW image is always uncalibrated. */ setImageColorWorkSpace(WORKSPACE_UNCALIBRATED); return true; } return false; } int DMetadata::getMSecsInfo() const { int ms = 0; bool ok = mSecTimeStamp("Exif.Photo.SubSecTime", ms); if (ok) return ms; ok = mSecTimeStamp("Exif.Photo.SubSecTimeOriginal", ms); if (ok) return ms; ok = mSecTimeStamp("Exif.Photo.SubSecTimeDigitized", ms); if (ok) return ms; return 0; } bool DMetadata::mSecTimeStamp(const char* const exifTagName, int& ms) const { bool ok = false; QString val = getExifTagString(exifTagName); if (!val.isEmpty()) { int sub = val.toUInt(&ok); if (ok) { int _ms = (int)(QString::fromLatin1("0.%1").arg(sub).toFloat(&ok) * 1000.0); if (ok) { ms = _ms; qCDebug(DIGIKAM_METAENGINE_LOG) << "msec timestamp: " << ms; } } } return ok; } -CaptionsMap DMetadata::getImageComments(const DMetadataSettingsContainer &settings) const +CaptionsMap DMetadata::getImageComments(const DMetadataSettingsContainer& settings) const { if (getFilePath().isEmpty()) { return CaptionsMap(); } CaptionsMap captionsMap; MetaEngine::AltLangMap authorsMap; MetaEngine::AltLangMap datesMap; MetaEngine::AltLangMap commentsMap; QString commonAuthor; // In first try to get captions properties from digiKam XMP namespace if (supportXmp()) { authorsMap = getXmpTagStringListLangAlt("Xmp.digiKam.CaptionsAuthorNames", false); datesMap = getXmpTagStringListLangAlt("Xmp.digiKam.CaptionsDateTimeStamps", false); if (authorsMap.isEmpty() && commonAuthor.isEmpty()) { QString xmpAuthors = getXmpTagString("Xmp.acdsee.author", false); if (!xmpAuthors.isEmpty()) { authorsMap.insert(QLatin1String("x-default"), xmpAuthors); } } } // Get author name from IPTC DescriptionWriter. Private namespace above gets precedence. QVariant descriptionWriter = getMetadataField(MetadataInfo::DescriptionWriter); if (!descriptionWriter.isNull()) { commonAuthor = descriptionWriter.toString(); } // In first, we check XMP alternative language tags to create map of values. bool xmpSupported = hasXmp(); bool iptcSupported = hasIptc(); bool exivSupported = hasExif(); for (NamespaceEntry entry : settings.getReadMapping(QLatin1String(DM_COMMENT_CONTAINER))) { if (entry.isDisabled) continue; QString commentString; const std::string myStr = entry.namespaceName.toStdString(); const char* nameSpace = myStr.data(); switch(entry.subspace) { case NamespaceEntry::XMP: switch(entry.specialOpts) { case NamespaceEntry::COMMENT_ALTLANG: if (xmpSupported) commentString = getXmpTagStringLangAlt(nameSpace, QString(), false); break; case NamespaceEntry::COMMENT_ATLLANGLIST: if (xmpSupported) commentsMap = getXmpTagStringListLangAlt(nameSpace, false); break; case NamespaceEntry::COMMENT_XMP: if (xmpSupported) commentString = getXmpTagString("Xmp.acdsee.notes", false); break; case NamespaceEntry::COMMENT_JPEG: // Now, we trying to get image comments, outside of XMP. // For JPEG, string is extracted from JFIF Comments section. // For PNG, string is extracted from iTXt chunk. commentString = getCommentsDecoded(); default: break; } break; case NamespaceEntry::IPTC: if (iptcSupported) commentString = getIptcTagString(nameSpace, false); break; case NamespaceEntry::EXIF: if (exivSupported) commentString = getExifComment(); break; default: break; } if (!commentString.isEmpty() &&!commentString.trimmed().isEmpty()) { commentsMap.insert(QLatin1String("x-default"), commentString); captionsMap.setData(commentsMap, authorsMap, commonAuthor, datesMap); return captionsMap; } if (!commentsMap.isEmpty()) { captionsMap.setData(commentsMap, authorsMap, commonAuthor, datesMap); return captionsMap; } } return captionsMap; } bool DMetadata::setImageComments(const CaptionsMap& comments, const DMetadataSettingsContainer &settings) const { /* // See bug #139313: An empty string is also a valid value if (comments.isEmpty()) return false; */ qCDebug(DIGIKAM_METAENGINE_LOG) << getFilePath() << " ==> Comment: " << comments; // In first, set captions properties to digiKam XMP namespace if (supportXmp()) { if (!setXmpTagStringListLangAlt("Xmp.digiKam.CaptionsAuthorNames", comments.authorsList())) { return false; } QString defaultAuthor = comments.value(QLatin1String("x-default")).author; removeXmpTag("Xmp.acdsee.author"); if (!defaultAuthor.isNull()) { if (!setXmpTagString("Xmp.acdsee.author", defaultAuthor)) { return false; } } if (!setXmpTagStringListLangAlt("Xmp.digiKam.CaptionsDateTimeStamps", comments.datesList())) { return false; } } QString defaultComment = comments.value(QLatin1String("x-default")).caption; QList toWrite = settings.getReadMapping(QLatin1String(DM_COMMENT_CONTAINER)); if (!settings.unifyReadWrite()) toWrite = settings.getWriteMapping(QLatin1String(DM_COMMENT_CONTAINER)); for (NamespaceEntry entry : toWrite) { if (entry.isDisabled) continue; const std::string myStr = entry.namespaceName.toStdString(); const char* nameSpace = myStr.data(); switch(entry.subspace) { case NamespaceEntry::XMP: if (entry.namespaceName.contains(QLatin1String("Xmp."))) removeXmpTag(nameSpace); switch(entry.specialOpts) { case NamespaceEntry::COMMENT_ALTLANG: if (!defaultComment.isNull()) { if (!setXmpTagStringLangAlt(nameSpace, defaultComment, QString())) { qCDebug(DIGIKAM_METAENGINE_LOG) << "Setting image comment failed" << nameSpace; return false; } } break; case NamespaceEntry::COMMENT_ATLLANGLIST: if (!setXmpTagStringListLangAlt(nameSpace, comments.toAltLangMap())) { return false; } break; case NamespaceEntry::COMMENT_XMP: if (!defaultComment.isNull()) { if (!setXmpTagString(nameSpace, defaultComment)) { return false; } } break; case NamespaceEntry::COMMENT_JPEG: // In first we set image comments, outside of Exif, XMP, and IPTC. if (!setComments(defaultComment.toUtf8())) { return false; } break; default: break; } break; case NamespaceEntry::IPTC: removeIptcTag(nameSpace); if (!defaultComment.isNull()) { defaultComment.truncate(2000); if (!setIptcTagString(nameSpace, defaultComment)) { return false; } } break; case NamespaceEntry::EXIF: if (!setExifComment(defaultComment)) { return false; } break; default: break; } } return true; } int DMetadata::getImagePickLabel() const { if (getFilePath().isEmpty()) { return -1; } if (hasXmp()) { QString value = getXmpTagString("Xmp.digiKam.PickLabel", false); if (!value.isEmpty()) { bool ok = false; long pickId = value.toLong(&ok); if (ok && pickId >= NoPickLabel && pickId <= AcceptedLabel) { return pickId; } } } return -1; } int DMetadata::getImageColorLabel() const { if (getFilePath().isEmpty()) { return -1; } if (hasXmp()) { QString value = getXmpTagString("Xmp.digiKam.ColorLabel", false); if (value.isEmpty()) { // Nikon NX use this XMP tags to store Color Labels value = getXmpTagString("Xmp.photoshop.Urgency", false); } if (!value.isEmpty()) { bool ok = false; long colorId = value.toLong(&ok); if (ok && colorId >= NoColorLabel && colorId <= WhiteLabel) { return colorId; } } // LightRoom use this tag to store color name as string. // Values are limited : see bug #358193. value = getXmpTagString("Xmp.xmp.Label", false); if (value == QLatin1String("Blue")) { return BlueLabel; } else if (value == QLatin1String("Green")) { return GreenLabel; } else if (value == QLatin1String("Red")) { return RedLabel; } else if (value == QLatin1String("Yellow")) { return YellowLabel; } else if (value == QLatin1String("Purple")) { return MagentaLabel; } } return -1; } CaptionsMap DMetadata::getImageTitles() const { if (getFilePath().isEmpty()) return CaptionsMap(); CaptionsMap captionsMap; MetaEngine::AltLangMap authorsMap; MetaEngine::AltLangMap datesMap; MetaEngine::AltLangMap titlesMap; QString commonAuthor; // Get author name from IPTC DescriptionWriter. Private namespace above gets precedence. QVariant descriptionWriter = getMetadataField(MetadataInfo::DescriptionWriter); if (!descriptionWriter.isNull()) commonAuthor = descriptionWriter.toString(); // In first, we check XMP alternative language tags to create map of values. if (hasXmp()) { titlesMap = getXmpTagStringListLangAlt("Xmp.dc.title", false); if (!titlesMap.isEmpty()) { captionsMap.setData(titlesMap, authorsMap, commonAuthor, datesMap); return captionsMap; } QString xmpTitle = getXmpTagString("Xmp.acdsee.caption" ,false); if (!xmpTitle.isEmpty() && !xmpTitle.trimmed().isEmpty()) { titlesMap.insert(QLatin1String("x-default"), xmpTitle); captionsMap.setData(titlesMap, authorsMap, commonAuthor, datesMap); return captionsMap; } } // We trying to get IPTC title if (hasIptc()) { QString iptcTitle = getIptcTagString("Iptc.Application2.ObjectName", false); if (!iptcTitle.isEmpty() && !iptcTitle.trimmed().isEmpty()) { titlesMap.insert(QLatin1String("x-default"), iptcTitle); captionsMap.setData(titlesMap, authorsMap, commonAuthor, datesMap); return captionsMap; } } return captionsMap; } bool DMetadata::setImageTitles(const CaptionsMap& titles) const { qCDebug(DIGIKAM_METAENGINE_LOG) << getFilePath() << " ==> Title: " << titles; QString defaultTitle = titles[QLatin1String("x-default")].caption; // In First we write comments into XMP. Language Alternative rule is not yet used. if (supportXmp()) { // NOTE : setXmpTagStringListLangAlt remove xmp tag before to add new values if (!setXmpTagStringListLangAlt("Xmp.dc.title", titles.toAltLangMap())) { return false; } removeXmpTag("Xmp.acdsee.caption"); if (!defaultTitle.isEmpty()) { if (!setXmpTagString("Xmp.acdsee.caption", defaultTitle)) { return false; } } } // In Second we write comments into IPTC. // Note that Caption IPTC tag is limited to 64 char and ASCII charset. removeIptcTag("Iptc.Application2.ObjectName"); if (!defaultTitle.isNull()) { defaultTitle.truncate(64); // See if we have any non printable chars in there. If so, skip IPTC // to avoid confusing other apps and web services with invalid tags. bool hasInvalidChar = false; for (QString::const_iterator c = defaultTitle.constBegin(); c != defaultTitle.constEnd(); ++c) { if (!(*c).isPrint()) { hasInvalidChar = true; break; } } if (!hasInvalidChar) { if (!setIptcTagString("Iptc.Application2.ObjectName", defaultTitle)) return false; } } return true; } int DMetadata::getImageRating(const DMetadataSettingsContainer &settings) const { if (getFilePath().isEmpty()) { return -1; } long rating = -1; bool xmpSupported = hasXmp(); bool iptcSupported = hasIptc(); bool exivSupported = hasExif(); for (NamespaceEntry entry : settings.getReadMapping(QLatin1String(DM_RATING_CONTAINER))) { if (entry.isDisabled) continue; const std::string myStr = entry.namespaceName.toStdString(); const char* nameSpace = myStr.data(); QString value; switch(entry.subspace) { case NamespaceEntry::XMP: if (xmpSupported) value = getXmpTagString(nameSpace, false); break; case NamespaceEntry::IPTC: if (iptcSupported) value = QString::fromUtf8(getIptcTagData(nameSpace)); break; case NamespaceEntry::EXIF: if (exivSupported) getExifTagLong(nameSpace, rating); break; default: break; } if (!value.isEmpty()) { bool ok = false; rating = value.toLong(&ok); if (!ok) { return -1; } } int index = entry.convertRatio.indexOf(rating); // Exact value was not found,but rating is in range, // so we try to aproximate it if ((index == -1) && (rating > entry.convertRatio.first()) && (rating < entry.convertRatio.last())) { for (int i = 0 ; i < entry.convertRatio.size() ; i++) { if (rating > entry.convertRatio.at(i)) { index = i; } } } if (index != -1) { return index; } } return -1; } bool DMetadata::setImagePickLabel(int pickId) const { if (pickId < NoPickLabel || pickId > AcceptedLabel) { qCDebug(DIGIKAM_METAENGINE_LOG) << "Pick Label value to write is out of range!"; return false; } qCDebug(DIGIKAM_METAENGINE_LOG) << getFilePath() << " ==> Pick Label: " << pickId; if (supportXmp()) { if (!setXmpTagString("Xmp.digiKam.PickLabel", QString::number(pickId))) { return false; } } return true; } bool DMetadata::setImageColorLabel(int colorId) const { if (colorId < NoColorLabel || colorId > WhiteLabel) { qCDebug(DIGIKAM_METAENGINE_LOG) << "Color Label value to write is out of range!"; return false; } qCDebug(DIGIKAM_METAENGINE_LOG) << getFilePath() << " ==> Color Label: " << colorId; if (supportXmp()) { if (!setXmpTagString("Xmp.digiKam.ColorLabel", QString::number(colorId))) { return false; } // Nikon NX use this XMP tags to store Color Labels if (!setXmpTagString("Xmp.photoshop.Urgency", QString::number(colorId))) { return false; } // LightRoom use this XMP tags to store Color Labels name // Values are limited : see bug #358193. QString LRLabel; switch(colorId) { case BlueLabel: LRLabel = QLatin1String("Blue"); break; case GreenLabel: LRLabel = QLatin1String("Green"); break; case RedLabel: LRLabel = QLatin1String("Red"); break; case YellowLabel: LRLabel = QLatin1String("Yellow"); break; case MagentaLabel: LRLabel = QLatin1String("Purple"); break; } if (!LRLabel.isEmpty()) { if (!setXmpTagString("Xmp.xmp.Label", LRLabel)) { return false; } } } return true; } bool DMetadata::setImageRating(int rating, const DMetadataSettingsContainer &settings) const { // NOTE : with digiKam 0.9.x, we have used IPTC Urgency to store Rating. // Now this way is obsolete, and we use standard XMP rating tag instead. if (rating < RatingMin || rating > RatingMax) { qCDebug(DIGIKAM_METAENGINE_LOG) << "Rating value to write is out of range!"; return false; } qCDebug(DIGIKAM_METAENGINE_LOG) << getFilePath() << " ==> Rating: +++++++++++" << rating; QList toWrite = settings.getReadMapping(QLatin1String(DM_RATING_CONTAINER)); if (!settings.unifyReadWrite()) toWrite = settings.getWriteMapping(QLatin1String(DM_RATING_CONTAINER)); for (NamespaceEntry entry : toWrite) { if (entry.isDisabled) continue; const std::string myStr = entry.namespaceName.toStdString(); const char* nameSpace = myStr.data(); switch(entry.subspace) { case NamespaceEntry::XMP: if (!setXmpTagString(nameSpace, QString::number(entry.convertRatio.at(rating)))) { qCDebug(DIGIKAM_METAENGINE_LOG) << "Setting rating failed" << nameSpace; return false; } break; case NamespaceEntry::EXIF: if (!setExifTagLong(nameSpace, rating)) { return false; } break; case NamespaceEntry::IPTC: // IPTC rating deprecated default: break; } } // Set Exif rating tag used by Windows Vista. if (!setExifTagLong("Exif.Image.0x4746", rating)) { return false; } // Wrapper around rating percents managed by Windows Vista. int ratePercents = 0; switch (rating) { case 0: ratePercents = 0; break; case 1: ratePercents = 1; break; case 2: ratePercents = 25; break; case 3: ratePercents = 50; break; case 4: ratePercents = 75; break; case 5: ratePercents = 99; break; } if (!setExifTagLong("Exif.Image.0x4749", ratePercents)) { return false; } return true; } bool DMetadata::setImageHistory(QString& imageHistoryXml) const { if (supportXmp()) { if (!setXmpTagString("Xmp.digiKam.ImageHistory", imageHistoryXml)) { return false; } else { return true; } } return false; } QString DMetadata::getImageHistory() const { if (hasXmp()) { QString value = getXmpTagString("Xmp.digiKam.ImageHistory", false); qCDebug(DIGIKAM_METAENGINE_LOG) << "Loading image history " << value; return value; } return QString(); } bool DMetadata::hasImageHistoryTag() const { if (hasXmp()) { if (QString(getXmpTagString("Xmp.digiKam.ImageHistory", false)).length() > 0) { return true; } else { return false; } } return false; } QString DMetadata::getImageUniqueId() const { QString exifUid; if (hasXmp()) { QString uuid = getXmpTagString("Xmp.digiKam.ImageUniqueID"); if (!uuid.isEmpty()) { return uuid; } exifUid = getXmpTagString("Xmp.exif.ImageUniqueId"); } if (exifUid.isEmpty()) { exifUid = getExifTagString("Exif.Photo.ImageUniqueID"); } // same makers may choose to use a "click counter" to generate the id, // which is then weak and not a universally unique id // The Exif ImageUniqueID is 128bit, or 32 hex digits. // If the first 20 are zero, it's probably a counter, // the left 12 are sufficient for more then 10^14 clicks. if (!exifUid.isEmpty() && !exifUid.startsWith(QLatin1String("00000000000000000000"))) { if (getExifTagString("Exif.Image.Make").contains(QLatin1String("SAMSUNG"), Qt::CaseInsensitive)) { // Generate for Samsung a new random 32 hex digits unique ID. QString imageUniqueID(QUuid::createUuid().toString()); imageUniqueID.remove(QLatin1Char('-')); imageUniqueID.remove(0, 1).chop(1); return imageUniqueID; } return exifUid; } // Exif.Image.ImageID can also be a pathname, so it's not sufficiently unique QString dngUid = getExifTagString("Exif.Image.RawDataUniqueID"); if (!dngUid.isEmpty()) { return dngUid; } return QString(); } bool DMetadata::setImageUniqueId(const QString& uuid) const { if (supportXmp()) { return setXmpTagString("Xmp.digiKam.ImageUniqueID", uuid); } return false; } PhotoInfoContainer DMetadata::getPhotographInformation() const { PhotoInfoContainer photoInfo; if (hasExif() || hasXmp()) { photoInfo.dateTime = getImageDateTime(); // ----------------------------------------------------------------------------------- photoInfo.make = getExifTagString("Exif.Image.Make"); if (photoInfo.make.isEmpty()) { photoInfo.make = getXmpTagString("Xmp.tiff.Make"); } // ----------------------------------------------------------------------------------- photoInfo.model = getExifTagString("Exif.Image.Model"); if (photoInfo.model.isEmpty()) { photoInfo.model = getXmpTagString("Xmp.tiff.Model"); } // ----------------------------------------------------------------------------------- photoInfo.lens = getLensDescription(); // ----------------------------------------------------------------------------------- photoInfo.aperture = getExifTagString("Exif.Photo.FNumber"); if (photoInfo.aperture.isEmpty()) { photoInfo.aperture = getExifTagString("Exif.Photo.ApertureValue"); } if (photoInfo.aperture.isEmpty()) { photoInfo.aperture = getXmpTagString("Xmp.exif.FNumber"); } if (photoInfo.aperture.isEmpty()) { photoInfo.aperture = getXmpTagString("Xmp.exif.ApertureValue"); } // ----------------------------------------------------------------------------------- photoInfo.exposureTime = getExifTagString("Exif.Photo.ExposureTime"); if (photoInfo.exposureTime.isEmpty()) { photoInfo.exposureTime = getExifTagString("Exif.Photo.ShutterSpeedValue"); } if (photoInfo.exposureTime.isEmpty()) { photoInfo.exposureTime = getXmpTagString("Xmp.exif.ExposureTime"); } if (photoInfo.exposureTime.isEmpty()) { photoInfo.exposureTime = getXmpTagString("Xmp.exif.ShutterSpeedValue"); } // ----------------------------------------------------------------------------------- photoInfo.exposureMode = getExifTagString("Exif.Photo.ExposureMode"); if (photoInfo.exposureMode.isEmpty()) { photoInfo.exposureMode = getXmpTagString("Xmp.exif.ExposureMode"); } if (photoInfo.exposureMode.isEmpty()) { photoInfo.exposureMode = getExifTagString("Exif.CanonCs.MeteringMode"); } // ----------------------------------------------------------------------------------- photoInfo.exposureProgram = getExifTagString("Exif.Photo.ExposureProgram"); if (photoInfo.exposureProgram.isEmpty()) { photoInfo.exposureProgram = getXmpTagString("Xmp.exif.ExposureProgram"); } if (photoInfo.exposureProgram.isEmpty()) { photoInfo.exposureProgram = getExifTagString("Exif.CanonCs.ExposureProgram"); } // ----------------------------------------------------------------------------------- photoInfo.focalLength = getExifTagString("Exif.Photo.FocalLength"); if (photoInfo.focalLength.isEmpty()) { photoInfo.focalLength = getXmpTagString("Xmp.exif.FocalLength"); } if (photoInfo.focalLength.isEmpty()) { photoInfo.focalLength = getExifTagString("Exif.Canon.FocalLength"); } // ----------------------------------------------------------------------------------- photoInfo.focalLength35mm = getExifTagString("Exif.Photo.FocalLengthIn35mmFilm"); if (photoInfo.focalLength35mm.isEmpty()) { photoInfo.focalLength35mm = getXmpTagString("Xmp.exif.FocalLengthIn35mmFilm"); } // ----------------------------------------------------------------------------------- QStringList ISOSpeedTags; ISOSpeedTags << QLatin1String("Exif.Photo.ISOSpeedRatings"); ISOSpeedTags << QLatin1String("Exif.Photo.ExposureIndex"); ISOSpeedTags << QLatin1String("Exif.Image.ISOSpeedRatings"); ISOSpeedTags << QLatin1String("Xmp.exif.ISOSpeedRatings"); ISOSpeedTags << QLatin1String("Xmp.exif.ExposureIndex"); ISOSpeedTags << QLatin1String("Exif.CanonSi.ISOSpeed"); ISOSpeedTags << QLatin1String("Exif.CanonCs.ISOSpeed"); ISOSpeedTags << QLatin1String("Exif.Nikon1.ISOSpeed"); ISOSpeedTags << QLatin1String("Exif.Nikon2.ISOSpeed"); ISOSpeedTags << QLatin1String("Exif.Nikon3.ISOSpeed"); ISOSpeedTags << QLatin1String("Exif.NikonIi.ISO"); ISOSpeedTags << QLatin1String("Exif.NikonIi.ISO2"); ISOSpeedTags << QLatin1String("Exif.MinoltaCsNew.ISOSetting"); ISOSpeedTags << QLatin1String("Exif.MinoltaCsOld.ISOSetting"); ISOSpeedTags << QLatin1String("Exif.MinoltaCs5D.ISOSpeed"); ISOSpeedTags << QLatin1String("Exif.MinoltaCs7D.ISOSpeed"); ISOSpeedTags << QLatin1String("Exif.Sony1Cs.ISOSetting"); ISOSpeedTags << QLatin1String("Exif.Sony2Cs.ISOSetting"); ISOSpeedTags << QLatin1String("Exif.Sony1Cs2.ISOSetting"); ISOSpeedTags << QLatin1String("Exif.Sony2Cs2.ISOSetting"); ISOSpeedTags << QLatin1String("Exif.Sony1MltCsA100.ISOSetting"); ISOSpeedTags << QLatin1String("Exif.Pentax.ISO"); ISOSpeedTags << QLatin1String("Exif.Olympus.ISOSpeed"); ISOSpeedTags << QLatin1String("Exif.Samsung2.ISO"); photoInfo.sensitivity = getExifTagStringFromTagsList(ISOSpeedTags); // ----------------------------------------------------------------------------------- photoInfo.flash = getExifTagString("Exif.Photo.Flash"); if (photoInfo.flash.isEmpty()) { photoInfo.flash = getXmpTagString("Xmp.exif.Flash"); } if (photoInfo.flash.isEmpty()) { photoInfo.flash = getExifTagString("Exif.CanonCs.FlashActivity"); } // ----------------------------------------------------------------------------------- photoInfo.whiteBalance = getExifTagString("Exif.Photo.WhiteBalance"); if (photoInfo.whiteBalance.isEmpty()) { photoInfo.whiteBalance = getXmpTagString("Xmp.exif.WhiteBalance"); } // ----------------------------------------------------------------------------------- double l, L, a; photoInfo.hasCoordinates = getGPSInfo(a, l, L); } return photoInfo; } VideoInfoContainer DMetadata::getVideoInformation() const { VideoInfoContainer videoInfo; if (hasXmp()) { if (videoInfo.aspectRatio.isEmpty()) { videoInfo.aspectRatio = getXmpTagString("Xmp.video.AspectRatio"); } if (videoInfo.audioBitRate.isEmpty()) { videoInfo.audioBitRate = getXmpTagString("Xmp.audio.SampleRate"); } if (videoInfo.audioChannelType.isEmpty()) { videoInfo.audioChannelType = getXmpTagString("Xmp.audio.ChannelType"); } if (videoInfo.audioCodec.isEmpty()) { videoInfo.audioCodec = getXmpTagString("Xmp.audio.Codec"); } if (videoInfo.duration.isEmpty()) { videoInfo.duration = getXmpTagString("Xmp.video.Duration"); } if (videoInfo.frameRate.isEmpty()) { videoInfo.frameRate = getXmpTagString("Xmp.video.FrameRate"); } if (videoInfo.videoCodec.isEmpty()) { videoInfo.videoCodec = getXmpTagString("Xmp.video.Codec"); } } return videoInfo; } bool DMetadata::getImageTagsPath(QStringList& tagsPath, const DMetadataSettingsContainer &settings) const { for (NamespaceEntry entry : settings.getReadMapping(QLatin1String(DM_TAG_CONTAINER))) { if (entry.isDisabled) continue; int index = 0; QString currentNamespace = entry.namespaceName; NamespaceEntry::SpecialOptions currentOpts = entry.specialOpts; // Some namespaces have altenative paths, we must search them both switch(entry.subspace) { case NamespaceEntry::XMP: while(index < 2) { const std::string myStr = currentNamespace.toStdString(); const char* nameSpace = myStr.data(); switch(currentOpts) { case NamespaceEntry::TAG_XMPBAG: tagsPath = getXmpTagStringBag(nameSpace, false); break; case NamespaceEntry::TAG_XMPSEQ: tagsPath = getXmpTagStringSeq(nameSpace, false); break; case NamespaceEntry::TAG_ACDSEE: getACDSeeTagsPath(tagsPath); break; // not used here, to suppress warnings case NamespaceEntry::COMMENT_XMP: case NamespaceEntry::COMMENT_ALTLANG: case NamespaceEntry::COMMENT_ATLLANGLIST: case NamespaceEntry::NO_OPTS: default: break; } if (!tagsPath.isEmpty()) { if (entry.separator != QLatin1String("/")) { tagsPath = tagsPath.replaceInStrings(entry.separator, QLatin1String("/")); } return true; } else if (!entry.alternativeName.isEmpty()) { currentNamespace = entry.alternativeName; currentOpts = entry.secondNameOpts; } else { break; // no alternative namespace, go to next one } index++; } break; case NamespaceEntry::IPTC: // Try to get Tags Path list from IPTC keywords. // digiKam 0.9.x has used IPTC keywords to store Tags Path list. // This way is obsolete now since digiKam support XMP because IPTC // do not support UTF-8 and have strings size limitation. But we will // let the capability to import it for interworking issues. tagsPath = getIptcKeywords(); if (!tagsPath.isEmpty()) { // Work around to Imach tags path list hosted in IPTC with '.' as separator. QStringList ntp = tagsPath.replaceInStrings(entry.separator, QLatin1String("/")); if (ntp != tagsPath) { tagsPath = ntp; qCDebug(DIGIKAM_METAENGINE_LOG) << "Tags Path imported from Imach: " << tagsPath; } return true; } break; case NamespaceEntry::EXIF: { // Try to get Tags Path list from Exif Windows keywords. QString keyWords = getExifTagString("Exif.Image.XPKeywords", false); if (!keyWords.isEmpty()) { tagsPath = keyWords.split(entry.separator); if (!tagsPath.isEmpty()) { return true; } } break; } default: break; } } return false; } bool DMetadata::setImageTagsPath(const QStringList& tagsPath, const DMetadataSettingsContainer& settings) const { // NOTE : with digiKam 0.9.x, we have used IPTC Keywords for that. // Now this way is obsolete, and we use XMP instead. // Set the new Tags path list. This is set, not add-to like setXmpKeywords. // Unlike the other keyword fields, we do not need to merge existing entries. QList toWrite = settings.getReadMapping(QLatin1String(DM_TAG_CONTAINER)); if (!settings.unifyReadWrite()) toWrite = settings.getWriteMapping(QLatin1String(DM_TAG_CONTAINER)); for (NamespaceEntry entry : toWrite) { if (entry.isDisabled) continue; QStringList newList; // get keywords from tags path, for type tag for (QString tagPath : tagsPath) { newList.append(tagPath.split(QLatin1String("/")).last()); } switch(entry.subspace) { case NamespaceEntry::XMP: if (supportXmp()) { if (entry.tagPaths != NamespaceEntry::TAG) { newList = tagsPath; if (entry.separator.compare(QLatin1String("/")) != 0) { newList = newList.replaceInStrings(QLatin1String("/"), entry.separator); } } const std::string myStr = entry.namespaceName.toStdString(); const char* nameSpace = myStr.data(); switch(entry.specialOpts) { case NamespaceEntry::TAG_XMPSEQ: if (!setXmpTagStringSeq(nameSpace, newList)) { qCDebug(DIGIKAM_METAENGINE_LOG) << "Setting image paths failed" << nameSpace; return false; } break; case NamespaceEntry::TAG_XMPBAG: if (!setXmpTagStringBag(nameSpace, newList)) { qCDebug(DIGIKAM_METAENGINE_LOG) << "Setting image paths failed" << nameSpace; return false; } break; case NamespaceEntry::TAG_ACDSEE: if (!setACDSeeTagsPath(newList)) { qCDebug(DIGIKAM_METAENGINE_LOG) << "Setting image paths failed" << nameSpace; return false; } default: break; } } break; case NamespaceEntry::IPTC: if (entry.namespaceName == QLatin1String("Iptc.Application2.Keywords")) { if (!setIptcKeywords(getIptcKeywords(), newList)) { qCDebug(DIGIKAM_METAENGINE_LOG) << "Setting image paths failed" << entry.namespaceName; return false; } } default: break; } } return true; } bool DMetadata::getACDSeeTagsPath(QStringList &tagsPath) const { // Try to get Tags Path list from ACDSee 8 Pro categories. QString xmlACDSee = getXmpTagString("Xmp.acdsee.categories", false); if (!xmlACDSee.isEmpty()) { xmlACDSee.remove(QLatin1String("")); xmlACDSee.remove(QLatin1String("")); xmlACDSee.replace(QLatin1String("/"), QLatin1String("\\")); QStringList xmlTags = xmlACDSee.split(QLatin1String("")); int length = tags.length() - (11 * count) - 5; if (category == 0) { tagsPath << tags.mid(5, length); } else { tagsPath.last().append(QLatin1String("/") + tags.mid(5, length)); } category = category - count + 1; if (tags.left(5) == QLatin1String("=\"1\">") && category > 0) { tagsPath << tagsPath.last().section(QLatin1String("/"), 0, category - 1); } } } if (!tagsPath.isEmpty()) { qCDebug(DIGIKAM_METAENGINE_LOG) << "Tags Path imported from ACDSee: " << tagsPath; return true; } } return false; } bool DMetadata::setACDSeeTagsPath(const QStringList &tagsPath) const { // Converting Tags path list to ACDSee 8 Pro categories. const QString category(QLatin1String("")); QStringList splitTags; QStringList xmlTags; foreach(const QString& tags, tagsPath) { splitTags = tags.split(QLatin1String("/")); int current = 0; for (int index = 0; index < splitTags.size(); index++) { int tagIndex = xmlTags.indexOf(category.arg(0) + splitTags[index]); if (tagIndex == -1) { tagIndex = xmlTags.indexOf(category.arg(1) + splitTags[index]); } splitTags[index].insert(0, category.arg(index == splitTags.size() - 1 ? 1 : 0)); if (tagIndex == -1) { if (index == 0) { xmlTags << splitTags[index]; xmlTags << QLatin1String(""); current = xmlTags.size() - 1; } else { xmlTags.insert(current, splitTags[index]); xmlTags.insert(current + 1, QLatin1String("")); current++; } } else { if (index == splitTags.size() - 1) { xmlTags[tagIndex] = splitTags[index]; } current = tagIndex + 1; } } } QString xmlACDSee = QLatin1String("") + xmlTags.join(QLatin1String("")) + QLatin1String(""); qCDebug(DIGIKAM_METAENGINE_LOG) << "xmlACDSee" << xmlACDSee; removeXmpTag("Xmp.acdsee.categories"); if (!xmlTags.isEmpty()) { if (!setXmpTagString("Xmp.acdsee.categories", xmlACDSee)) { return false; } } return true; } bool DMetadata::getImageFacesMap(QMultiMap& faces) const { faces.clear(); // The example code for Exiv2 says: // > There are no specialized values for structures, qualifiers and nested // > types. However, these can be added by using an XmpTextValue and a path as // > the key. // I think that means I have to iterate over the WLPG face tags in the clunky // way below (guess numbers and look them up as strings). (Leif) const QString personPathTemplate = QLatin1String("Xmp.MP.RegionInfo/MPRI:Regions[%1]/MPReg:PersonDisplayName"); const QString rectPathTemplate = QLatin1String("Xmp.MP.RegionInfo/MPRI:Regions[%1]/MPReg:Rectangle"); for (int i = 1; ; i++) { QString person = getXmpTagString(personPathTemplate.arg(i).toLatin1().constData(), false); if (person.isEmpty()) break; // The WLPG tags have the format X.XX, Y.YY, W.WW, H.HH // That is, four decimal numbers ranging from 0-1. // The top left position is indicated by X.XX, Y.YY (as a // percentage of the width/height of the entire image). // Similarly the width and height of the face's box are // indicated by W.WW and H.HH. QString rectString = getXmpTagString(rectPathTemplate.arg(i).toLatin1().constData(), false); QStringList list = rectString.split(QLatin1Char(',')); if (list.size() < 4) { qCDebug(DIGIKAM_METAENGINE_LOG) << "Cannot parse WLPG rectangle string" << rectString; continue; } QRectF rect(list.at(0).toFloat(), list.at(1).toFloat(), list.at(2).toFloat(), list.at(3).toFloat()); faces.insertMulti(person, rect); } /** Read face tags only if libkexiv can write them, otherwise * garbage tags will be generated on image transformation */ // Read face tags as saved by Picasa // http://www.exiv2.org/tags-xmp-mwg-rs.html const QString mwg_personPathTemplate = QLatin1String("Xmp.mwg-rs.Regions/mwg-rs:RegionList[%1]/mwg-rs:Name"); const QString mwg_rect_x_PathTemplate = QLatin1String("Xmp.mwg-rs.Regions/mwg-rs:RegionList[%1]/mwg-rs:Area/stArea:x"); const QString mwg_rect_y_PathTemplate = QLatin1String("Xmp.mwg-rs.Regions/mwg-rs:RegionList[%1]/mwg-rs:Area/stArea:y"); const QString mwg_rect_w_PathTemplate = QLatin1String("Xmp.mwg-rs.Regions/mwg-rs:RegionList[%1]/mwg-rs:Area/stArea:w"); const QString mwg_rect_h_PathTemplate = QLatin1String("Xmp.mwg-rs.Regions/mwg-rs:RegionList[%1]/mwg-rs:Area/stArea:h"); for (int i = 1; ; i++) { QString person = getXmpTagString(mwg_personPathTemplate.arg(i).toLatin1().constData(), false); if (person.isEmpty()) break; // x and y is the center point float x = getXmpTagString(mwg_rect_x_PathTemplate.arg(i).toLatin1().constData(), false).toFloat(); float y = getXmpTagString(mwg_rect_y_PathTemplate.arg(i).toLatin1().constData(), false).toFloat(); float w = getXmpTagString(mwg_rect_w_PathTemplate.arg(i).toLatin1().constData(), false).toFloat(); float h = getXmpTagString(mwg_rect_h_PathTemplate.arg(i).toLatin1().constData(), false).toFloat(); QRectF rect(x - w/2, y - h/2, w, h); faces.insertMulti(person, rect); qCDebug(DIGIKAM_METAENGINE_LOG) << "Found new rect " << person << " "<< rect; } return !faces.isEmpty(); } bool DMetadata::setImageFacesMap(QMultiMap< QString, QVariant >& facesPath, bool write) const { QString qxmpTagName(QLatin1String("Xmp.mwg-rs.Regions/mwg-rs:RegionList")); QString nameTagKey = qxmpTagName + QLatin1String("[%1]/mwg-rs:Name"); QString typeTagKey = qxmpTagName + QLatin1String("[%1]/mwg-rs:Type"); QString areaTagKey = qxmpTagName + QLatin1String("[%1]/mwg-rs:Area"); QString areaxTagKey = qxmpTagName + QLatin1String("[%1]/mwg-rs:Area/stArea:x"); QString areayTagKey = qxmpTagName + QLatin1String("[%1]/mwg-rs:Area/stArea:y"); QString areawTagKey = qxmpTagName + QLatin1String("[%1]/mwg-rs:Area/stArea:w"); QString areahTagKey = qxmpTagName + QLatin1String("[%1]/mwg-rs:Area/stArea:h"); QString areanormTagKey = qxmpTagName + QLatin1String("[%1]/mwg-rs:Area/stArea:unit"); QString winQxmpTagName = QLatin1String("Xmp.MP.RegionInfo/MPRI:Regions"); QString winRectTagKey = winQxmpTagName + QLatin1String("[%1]/MPReg:Rectangle"); QString winNameTagKey = winQxmpTagName + QLatin1String("[%1]/MPReg:PersonDisplayName"); if (!write) { QString check = getXmpTagString(nameTagKey.arg(1).toLatin1().constData()); if (check.isEmpty()) return true; } setXmpTagString(qxmpTagName.toLatin1().constData(), QString(), MetaEngine::XmpTagType(1)); setXmpTagString(winQxmpTagName.toLatin1().constData(), QString(), MetaEngine::XmpTagType(1)); QMap::const_iterator it = facesPath.constBegin(); int i = 1; bool ok = true; while (it != facesPath.constEnd()) { qreal x, y, w, h; it.value().toRectF().getRect(&x, &y, &w, &h); qCDebug(DIGIKAM_METAENGINE_LOG) << ">>>>>>>>>>set face meta:" << x << y << w << h; /** Write face tags in Windows Live Photo format **/ QString rectString; rectString.append(QString::number(x) + QLatin1String(", ")); rectString.append(QString::number(y) + QLatin1String(", ")); rectString.append(QString::number(w) + QLatin1String(", ")); rectString.append(QString::number(h)); /** Set tag rect **/ setXmpTagString(winRectTagKey.arg(i).toLatin1().constData(), rectString, MetaEngine::XmpTagType(0)); /** Set tag name **/ setXmpTagString(winNameTagKey.arg(i).toLatin1().constData(),it.key(), MetaEngine::XmpTagType(0)); /** Writing rectangle in Metadata Group format **/ x += w/2; y += h/2; /** Set tag name **/ ok &= setXmpTagString(nameTagKey.arg(i).toLatin1().constData(), it.key(),MetaEngine::XmpTagType(0)); /** Set tag type as Face **/ ok &= setXmpTagString(typeTagKey.arg(i).toLatin1().constData(), QLatin1String("Face"), MetaEngine::XmpTagType(0)); /** Set tag Area, with xmp type struct **/ ok &= setXmpTagString(areaTagKey.arg(i).toLatin1().constData(), QString(), MetaEngine::XmpTagType(2)); /** Set stArea:x inside Area structure **/ ok &= setXmpTagString(areaxTagKey.arg(i).toLatin1().constData(), QString::number(x), MetaEngine::XmpTagType(0)); /** Set stArea:y inside Area structure **/ ok &= setXmpTagString(areayTagKey.arg(i).toLatin1().constData(), QString::number(y), MetaEngine::XmpTagType(0)); /** Set stArea:w inside Area structure **/ ok &= setXmpTagString(areawTagKey.arg(i).toLatin1().constData(), QString::number(w), MetaEngine::XmpTagType(0)); /** Set stArea:h inside Area structure **/ ok &= setXmpTagString(areahTagKey.arg(i).toLatin1().constData(), QString::number(h), MetaEngine::XmpTagType(0)); /** Set stArea:unit inside Area structure as normalized **/ ok &= setXmpTagString(areanormTagKey.arg(i).toLatin1().constData(), QLatin1String("normalized"), MetaEngine::XmpTagType(0)); ++it; ++i; } return ok; } bool DMetadata::setMetadataTemplate(const Template& t) const { if (t.isNull()) { return false; } QStringList authors = t.authors(); QString authorsPosition = t.authorsPosition(); QString credit = t.credit(); QString source = t.source(); MetaEngine::AltLangMap copyright = t.copyright(); MetaEngine::AltLangMap rightUsage = t.rightUsageTerms(); QString instructions = t.instructions(); qCDebug(DIGIKAM_METAENGINE_LOG) << "Applying Metadata Template: " << t.templateTitle() << " :: " << authors; // Set XMP tags. XMP<->IPTC Schema from Photoshop 7.0 if (supportXmp()) { if (!setXmpTagStringSeq("Xmp.dc.creator", authors)) { return false; } if (!setXmpTagStringSeq("Xmp.tiff.Artist", authors)) { return false; } if (!setXmpTagString("Xmp.photoshop.AuthorsPosition", authorsPosition)) { return false; } if (!setXmpTagString("Xmp.photoshop.Credit", credit)) { return false; } if (!setXmpTagString("Xmp.photoshop.Source", source)) { return false; } if (!setXmpTagString("Xmp.dc.source", source)) { return false; } if (!setXmpTagStringListLangAlt("Xmp.dc.rights", copyright)) { return false; } if (!setXmpTagStringListLangAlt("Xmp.tiff.Copyright", copyright)) { return false; } if (!setXmpTagStringListLangAlt("Xmp.xmpRights.UsageTerms", rightUsage)) { return false; } if (!setXmpTagString("Xmp.photoshop.Instructions", instructions)) { return false; } } // Set IPTC tags. if (!setIptcTagsStringList("Iptc.Application2.Byline", 32, getIptcTagsStringList("Iptc.Application2.Byline"), authors)) { return false; } if (!setIptcTag(authorsPosition, 32, "Authors Title", "Iptc.Application2.BylineTitle")) { return false; } if (!setIptcTag(credit, 32, "Credit", "Iptc.Application2.Credit")) { return false; } if (!setIptcTag(source, 32, "Source", "Iptc.Application2.Source")) { return false; } if (!setIptcTag(copyright[QLatin1String("x-default")], 128, "Copyright", "Iptc.Application2.Copyright")) { return false; } if (!setIptcTag(instructions, 256, "Instructions", "Iptc.Application2.SpecialInstructions")) { return false; } if (!setIptcCoreLocation(t.locationInfo())) { return false; } if (!setCreatorContactInfo(t.contactInfo())) { return false; } if (supportXmp()) { if (!setXmpSubjects(t.IptcSubjects())) { return false; } } // Synchronize Iptc subjects tags with Xmp subjects tags. QStringList list = t.IptcSubjects(); QStringList newList; foreach(QString str, list) // krazy:exclude=foreach { if (str.startsWith(QLatin1String("XMP"))) { str.replace(0, 3, QLatin1String("IPTC")); } newList.append(str); } if (!setIptcSubjects(getIptcSubjects(), newList)) { return false; } return true; } bool DMetadata::removeMetadataTemplate() const { // Remove Rights info. removeXmpTag("Xmp.dc.creator"); removeXmpTag("Xmp.tiff.Artist"); removeXmpTag("Xmp.photoshop.AuthorsPosition"); removeXmpTag("Xmp.photoshop.Credit"); removeXmpTag("Xmp.photoshop.Source"); removeXmpTag("Xmp.dc.source"); removeXmpTag("Xmp.dc.rights"); removeXmpTag("Xmp.tiff.Copyright"); removeXmpTag("Xmp.xmpRights.UsageTerms"); removeXmpTag("Xmp.photoshop.Instructions"); removeIptcTag("Iptc.Application2.Byline"); removeIptcTag("Iptc.Application2.BylineTitle"); removeIptcTag("Iptc.Application2.Credit"); removeIptcTag("Iptc.Application2.Source"); removeIptcTag("Iptc.Application2.Copyright"); removeIptcTag("Iptc.Application2.SpecialInstructions"); // Remove Location info. removeXmpTag("Xmp.photoshop.Country"); removeXmpTag("Xmp.iptc.CountryCode"); removeXmpTag("Xmp.photoshop.City"); removeXmpTag("Xmp.iptc.Location"); removeXmpTag("Xmp.photoshop.State"); removeIptcTag("Iptc.Application2.CountryName"); removeIptcTag("Iptc.Application2.CountryCode"); removeIptcTag("Iptc.Application2.City"); removeIptcTag("Iptc.Application2.SubLocation"); removeIptcTag("Iptc.Application2.ProvinceState"); // Remove Contact info. removeXmpTag("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCity"); removeXmpTag("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCtry"); removeXmpTag("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrExtadr"); removeXmpTag("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrPcode"); removeXmpTag("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrRegion"); removeXmpTag("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiEmailWork"); removeXmpTag("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiTelWork"); removeXmpTag("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiUrlWork"); // Remove IPTC Subjects. removeXmpTag("Xmp.iptc.SubjectCode"); removeIptcTag("Iptc.Application2.Subject"); return true; } Template DMetadata::getMetadataTemplate() const { Template t; getCopyrightInformation(t); t.setLocationInfo(getIptcCoreLocation()); t.setIptcSubjects(getIptcCoreSubjects()); // get from XMP or Iptc return t; } static bool hasValidField(const QVariantList& list) { for (QVariantList::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) { if (!(*it).isNull()) { return true; } } return false; } bool DMetadata::getCopyrightInformation(Template& t) const { MetadataFields fields; fields << MetadataInfo::IptcCoreCopyrightNotice << MetadataInfo::IptcCoreCreator << MetadataInfo::IptcCoreProvider << MetadataInfo::IptcCoreRightsUsageTerms << MetadataInfo::IptcCoreSource << MetadataInfo::IptcCoreCreatorJobTitle << MetadataInfo::IptcCoreInstructions; QVariantList metadataInfos = getMetadataFields(fields); IptcCoreContactInfo contactInfo = getCreatorContactInfo(); if (!hasValidField(metadataInfos) && contactInfo.isNull()) { return false; } t.setCopyright(toAltLangMap(metadataInfos.at(0))); t.setAuthors(metadataInfos.at(1).toStringList()); t.setCredit(metadataInfos.at(2).toString()); t.setRightUsageTerms(toAltLangMap(metadataInfos.at(3))); t.setSource(metadataInfos.at(4).toString()); t.setAuthorsPosition(metadataInfos.at(5).toString()); t.setInstructions(metadataInfos.at(6).toString()); t.setContactInfo(contactInfo); return true; } IptcCoreContactInfo DMetadata::getCreatorContactInfo() const { MetadataFields fields; fields << MetadataInfo::IptcCoreContactInfoCity << MetadataInfo::IptcCoreContactInfoCountry << MetadataInfo::IptcCoreContactInfoAddress << MetadataInfo::IptcCoreContactInfoPostalCode << MetadataInfo::IptcCoreContactInfoProvinceState << MetadataInfo::IptcCoreContactInfoEmail << MetadataInfo::IptcCoreContactInfoPhone << MetadataInfo::IptcCoreContactInfoWebUrl; QVariantList metadataInfos = getMetadataFields(fields); IptcCoreContactInfo info; if (metadataInfos.size() == 8) { info.city = metadataInfos.at(0).toString(); info.country = metadataInfos.at(1).toString(); info.address = metadataInfos.at(2).toString(); info.postalCode = metadataInfos.at(3).toString(); info.provinceState = metadataInfos.at(4).toString(); info.email = metadataInfos.at(5).toString(); info.phone = metadataInfos.at(6).toString(); info.webUrl = metadataInfos.at(7).toString(); } return info; } bool DMetadata::setCreatorContactInfo(const IptcCoreContactInfo& info) const { if (!supportXmp()) { return false; } if (!setXmpTagString("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCity", info.city)) { return false; } if (!setXmpTagString("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCtry", info.country)) { return false; } if (!setXmpTagString("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrExtadr", info.address)) { return false; } if (!setXmpTagString("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrPcode", info.postalCode)) { return false; } if (!setXmpTagString("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrRegion", info.provinceState)) { return false; } if (!setXmpTagString("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiEmailWork", info.email)) { return false; } if (!setXmpTagString("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiTelWork", info.phone)) { return false; } if (!setXmpTagString("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiUrlWork", info.webUrl)) { return false; } return true; } IptcCoreLocationInfo DMetadata::getIptcCoreLocation() const { MetadataFields fields; fields << MetadataInfo::IptcCoreCountry << MetadataInfo::IptcCoreCountryCode << MetadataInfo::IptcCoreCity << MetadataInfo::IptcCoreLocation << MetadataInfo::IptcCoreProvinceState; QVariantList metadataInfos = getMetadataFields(fields); IptcCoreLocationInfo location; if (fields.size() == 5) { location.country = metadataInfos.at(0).toString(); location.countryCode = metadataInfos.at(1).toString(); location.city = metadataInfos.at(2).toString(); location.location = metadataInfos.at(3).toString(); location.provinceState = metadataInfos.at(4).toString(); } return location; } bool DMetadata::setIptcCoreLocation(const IptcCoreLocationInfo& location) const { if (supportXmp()) { if (!setXmpTagString("Xmp.photoshop.Country", location.country)) { return false; } if (!setXmpTagString("Xmp.iptc.CountryCode", location.countryCode)) { return false; } if (!setXmpTagString("Xmp.photoshop.City", location.city)) { return false; } if (!setXmpTagString("Xmp.iptc.Location", location.location)) { return false; } if (!setXmpTagString("Xmp.photoshop.State", location.provinceState)) { return false; } } if (!setIptcTag(location.country, 64, "Country", "Iptc.Application2.CountryName")) { return false; } if (!setIptcTag(location.countryCode, 3, "Country Code", "Iptc.Application2.CountryCode")) { return false; } if (!setIptcTag(location.city, 32, "City", "Iptc.Application2.City")) { return false; } if (!setIptcTag(location.location, 32, "SubLocation", "Iptc.Application2.SubLocation")) { return false; } if (!setIptcTag(location.provinceState, 32, "Province/State", "Iptc.Application2.ProvinceState")) { return false; } return true; } QStringList DMetadata::getIptcCoreSubjects() const { QStringList list = getXmpSubjects(); if (!list.isEmpty()) { return list; } return getIptcSubjects(); } QString DMetadata::getLensDescription() const { QString lens; QStringList lensExifTags; // In first, try to get Lens information from makernotes. lensExifTags.append(QLatin1String("Exif.CanonCs.LensType")); // Canon Cameras Makernote. lensExifTags.append(QLatin1String("Exif.CanonCs.Lens")); // Canon Cameras Makernote. lensExifTags.append(QLatin1String("Exif.Canon.0x0095")); // Alternative Canon Cameras Makernote. lensExifTags.append(QLatin1String("Exif.NikonLd1.LensIDNumber")); // Nikon Cameras Makernote. lensExifTags.append(QLatin1String("Exif.NikonLd2.LensIDNumber")); // Nikon Cameras Makernote. lensExifTags.append(QLatin1String("Exif.NikonLd3.LensIDNumber")); // Nikon Cameras Makernote. lensExifTags.append(QLatin1String("Exif.Minolta.LensID")); // Minolta Cameras Makernote. lensExifTags.append(QLatin1String("Exif.Photo.LensModel")); // Sony Cameras Makernote (and others?). lensExifTags.append(QLatin1String("Exif.Sony1.LensID")); // Sony Cameras Makernote. lensExifTags.append(QLatin1String("Exif.Sony2.LensID")); // Sony Cameras Makernote. lensExifTags.append(QLatin1String("Exif.SonyMinolta.LensID")); // Sony Cameras Makernote. lensExifTags.append(QLatin1String("Exif.Pentax.LensType")); // Pentax Cameras Makernote. lensExifTags.append(QLatin1String("Exif.PentaxDng.LensType")); // Pentax Cameras Makernote. lensExifTags.append(QLatin1String("Exif.Panasonic.0x0051")); // Panasonic Cameras Makernote. lensExifTags.append(QLatin1String("Exif.Panasonic.0x0310")); // Panasonic Cameras Makernote. lensExifTags.append(QLatin1String("Exif.Sigma.LensRange")); // Sigma Cameras Makernote. lensExifTags.append(QLatin1String("Exif.Samsung2.LensType")); // Samsung Cameras Makernote. lensExifTags.append(QLatin1String("Exif.Photo.0xFDEA")); // Non-standard Exif tag set by Camera Raw. lensExifTags.append(QLatin1String("Exif.OlympusEq.LensModel")); // Olympus Cameras Makernote. // Olympus Cameras Makernote. FIXME is this necessary? exiv2 returns complete name, which doesn't match with lensfun information, see bug #311295 //lensExifTags.append("Exif.OlympusEq.LensType"); // TODO : add Fuji camera Makernotes. // ------------------------------------------------------------------- // Try to get Lens Data information from Exif. for (QStringList::const_iterator it = lensExifTags.constBegin(); it != lensExifTags.constEnd(); ++it) { lens = getExifTagString((*it).toLatin1().constData()); if ( !lens.isEmpty() && !(lens.startsWith(QLatin1Char('(')) && lens.endsWith(QLatin1Char(')')) ) ) // To prevent undecoded tag values from Exiv2 as "(65535)". { return lens; } } // ------------------------------------------------------------------- // Try to get Lens Data information from XMP. // XMP aux tags. lens = getXmpTagString("Xmp.aux.Lens"); if (lens.isEmpty()) { // XMP M$ tags (Lens Maker + Lens Model). lens = getXmpTagString("Xmp.MicrosoftPhoto.LensManufacturer"); if (!lens.isEmpty()) { lens.append(QLatin1String(" ")); } lens.append(getXmpTagString("Xmp.MicrosoftPhoto.LensModel")); } return lens; } IccProfile DMetadata::getIccProfile() const { // Check if Exif data contains an ICC color profile. QByteArray data = getExifTagData("Exif.Image.InterColorProfile"); if (!data.isNull()) { qCDebug(DIGIKAM_METAENGINE_LOG) << "Found an ICC profile in Exif metadata"; return data; } // Else check the Exif color-space tag and use default profiles that we ship switch (getImageColorWorkSpace()) { case DMetadata::WORKSPACE_SRGB: { qCDebug(DIGIKAM_METAENGINE_LOG) << "Exif color-space tag is sRGB. Using default sRGB ICC profile."; return IccProfile::sRGB(); } case DMetadata::WORKSPACE_ADOBERGB: { qCDebug(DIGIKAM_METAENGINE_LOG) << "Exif color-space tag is AdobeRGB. Using default AdobeRGB ICC profile."; return IccProfile::adobeRGB(); } default: break; } return IccProfile(); } bool DMetadata::setIccProfile(const IccProfile& profile) { if (profile.isNull()) { removeExifTag("Exif.Image.InterColorProfile"); } else { QByteArray data = IccProfile(profile).data(); if (!setExifTagData("Exif.Image.InterColorProfile", data)) { return false; } } removeExifColorSpace(); return true; } bool DMetadata::setIptcTag(const QString& text, int maxLength, const char* const debugLabel, const char* const tagKey) const { QString truncatedText = text; truncatedText.truncate(maxLength); qCDebug(DIGIKAM_METAENGINE_LOG) << getFilePath() << " ==> " << debugLabel << ": " << truncatedText; return setIptcTagString(tagKey, truncatedText); // returns false if failed } inline QVariant DMetadata::fromExifOrXmp(const char* const exifTagName, const char* const xmpTagName) const { QVariant var; if (exifTagName) { var = getExifTagVariant(exifTagName, false); if (!var.isNull()) { return var; } } if (xmpTagName) { var = getXmpTagVariant(xmpTagName); if (!var.isNull()) { return var; } } return var; } inline QVariant DMetadata::fromIptcOrXmp(const char* const iptcTagName, const char* const xmpTagName) const { if (iptcTagName) { QString iptcValue = getIptcTagString(iptcTagName); if (!iptcValue.isNull()) { return iptcValue; } } if (xmpTagName) { QVariant var = getXmpTagVariant(xmpTagName); if (!var.isNull()) { return var; } } return QVariant(QVariant::String); } inline QVariant DMetadata::fromIptcEmulateList(const char* const iptcTagName) const { return toStringListVariant(getIptcTagsStringList(iptcTagName)); } inline QVariant DMetadata::fromXmpList(const char* const xmpTagName) const { QVariant var = getXmpTagVariant(xmpTagName); if (var.isNull()) { return QVariant(QVariant::StringList); } return var; } inline QVariant DMetadata::fromIptcEmulateLangAlt(const char* const iptcTagName) const { QString str = getIptcTagString(iptcTagName); if (str.isNull()) { return QVariant(QVariant::Map); } QMap map; map[QLatin1String("x-default")] = str; return map; } inline QVariant DMetadata::fromXmpLangAlt(const char* const xmpTagName) const { QVariant var = getXmpTagVariant(xmpTagName); if (var.isNull()) { return QVariant(QVariant::Map); } return var; } inline QVariant DMetadata::toStringListVariant(const QStringList& list) const { if (list.isEmpty()) { return QVariant(QVariant::StringList); } return list; } QVariant DMetadata::getMetadataField(MetadataInfo::Field field) const { switch (field) { case MetadataInfo::Comment: return getImageComments()[QLatin1String("x-default")].caption; case MetadataInfo::CommentJfif: return getCommentsDecoded(); case MetadataInfo::CommentExif: return getExifComment(); case MetadataInfo::CommentIptc: return fromIptcOrXmp("Iptc.Application2.Caption", 0); case MetadataInfo::Description: { QVariant var = fromXmpLangAlt("Xmp.dc.description"); if (!var.isNull()) { return var; } var = fromXmpLangAlt("Xmp.tiff.ImageDescription"); if (!var.isNull()) { return var; } return fromIptcEmulateLangAlt("Iptc.Application2.Caption"); } case MetadataInfo::Headline: return fromIptcOrXmp("Iptc.Application2.Headline", "Xmp.photoshop.Headline"); case MetadataInfo::Title: { QString str = getImageTitles()[QLatin1String("x-default")].caption; if (str.isEmpty()) { return QVariant(QVariant::Map); } QMap map; map[QLatin1String("x-default")] = str; return map; } case MetadataInfo::DescriptionWriter: return fromIptcOrXmp("Iptc.Application2.Writer", "Xmp.photoshop.CaptionWriter"); case MetadataInfo::Keywords: { QStringList list; getImageTagsPath(list); return toStringListVariant(list); } case MetadataInfo::Faces: { QMultiMap faceMap; getImageFacesMap(faceMap); QVariant var(faceMap); return var; } case MetadataInfo::Rating: return getImageRating(); case MetadataInfo::CreationDate: return getImageDateTime(); case MetadataInfo::DigitizationDate: return getDigitizationDateTime(true); case MetadataInfo::Orientation: return (int)getImageOrientation(); case MetadataInfo::Make: { QVariant var = fromExifOrXmp("Exif.Image.Make", "Xmp.tiff.Make"); return QVariant(var.toString().trimmed()); } case MetadataInfo::Model: { QVariant var = fromExifOrXmp("Exif.Image.Model", "Xmp.tiff.Model"); return QVariant(var.toString().trimmed()); } case MetadataInfo::Lens: return getLensDescription(); case MetadataInfo::Aperture: { QVariant var = fromExifOrXmp("Exif.Photo.FNumber", "Xmp.exif.FNumber"); if (var.isNull()) { var = fromExifOrXmp("Exif.Photo.ApertureValue", "Xmp.exif.ApertureValue"); if (!var.isNull()) { var = apexApertureToFNumber(var.toDouble()); } } return var; } case MetadataInfo::FocalLength: return fromExifOrXmp("Exif.Photo.FocalLength", "Xmp.exif.FocalLength"); case MetadataInfo::FocalLengthIn35mm: return fromExifOrXmp("Exif.Photo.FocalLengthIn35mmFilm", "Xmp.exif.FocalLengthIn35mmFilm"); case MetadataInfo::ExposureTime: { QVariant var = fromExifOrXmp("Exif.Photo.ExposureTime", "Xmp.exif.ExposureTime"); if (var.isNull()) { var = fromExifOrXmp("Exif.Photo.ShutterSpeedValue", "Xmp.exif.ShutterSpeedValue"); if (!var.isNull()) { var = apexShutterSpeedToExposureTime(var.toDouble()); } } return var; } case MetadataInfo::ExposureProgram: return fromExifOrXmp("Exif.Photo.ExposureProgram", "Xmp.exif.ExposureProgram"); case MetadataInfo::ExposureMode: return fromExifOrXmp("Exif.Photo.ExposureMode", "Xmp.exif.ExposureMode"); case MetadataInfo::Sensitivity: { QVariant var = fromExifOrXmp("Exif.Photo.ISOSpeedRatings", "Xmp.exif.ISOSpeedRatings"); //if (var.isNull()) // TODO: has this ISO format??? We must convert to the format of ISOSpeedRatings! // var = fromExifOrXmp("Exif.Photo.ExposureIndex", "Xmp.exif.ExposureIndex"); return var; } case MetadataInfo::FlashMode: return fromExifOrXmp("Exif.Photo.Flash", "Xmp.exif.Flash"); case MetadataInfo::WhiteBalance: return fromExifOrXmp("Exif.Photo.WhiteBalance", "Xmp.exif.WhiteBalance"); case MetadataInfo::MeteringMode: return fromExifOrXmp("Exif.Photo.MeteringMode", "Xmp.exif.MeteringMode"); case MetadataInfo::SubjectDistance: return fromExifOrXmp("Exif.Photo.SubjectDistance", "Xmp.exif.SubjectDistance"); case MetadataInfo::SubjectDistanceCategory: return fromExifOrXmp("Exif.Photo.SubjectDistanceRange", "Xmp.exif.SubjectDistanceRange"); case MetadataInfo::WhiteBalanceColorTemperature: //TODO: ?? return QVariant(QVariant::Int); case MetadataInfo::Longitude: return getGPSLongitudeString(); case MetadataInfo::LongitudeNumber: { double longitude; if (getGPSLongitudeNumber(&longitude)) { return longitude; } else { return QVariant(QVariant::Double); } } case MetadataInfo::Latitude: return getGPSLatitudeString(); case MetadataInfo::LatitudeNumber: { double latitude; if (getGPSLatitudeNumber(&latitude)) { return latitude; } else { return QVariant(QVariant::Double); } } case MetadataInfo::Altitude: { double altitude; if (getGPSAltitude(&altitude)) { return altitude; } else { return QVariant(QVariant::Double); } } case MetadataInfo::PositionOrientation: case MetadataInfo::PositionTilt: case MetadataInfo::PositionRoll: case MetadataInfo::PositionAccuracy: // TODO or unsupported? return QVariant(QVariant::Double); case MetadataInfo::PositionDescription: // TODO or unsupported? return QVariant(QVariant::String); case MetadataInfo::IptcCoreCopyrightNotice: { QVariant var = fromXmpLangAlt("Xmp.dc.rights"); if (!var.isNull()) { return var; } var = fromXmpLangAlt("Xmp.tiff.Copyright"); if (!var.isNull()) { return var; } return fromIptcEmulateLangAlt("Iptc.Application2.Copyright"); } case MetadataInfo::IptcCoreCreator: { QVariant var = fromXmpList("Xmp.dc.creator"); if (!var.isNull()) { return var; } QString artist = getXmpTagString("Xmp.tiff.Artist"); if (!artist.isNull()) { QStringList list; list << artist; return list; } return fromIptcEmulateList("Iptc.Application2.Byline"); } case MetadataInfo::IptcCoreProvider: return fromIptcOrXmp("Iptc.Application2.Credit", "Xmp.photoshop.Credit"); case MetadataInfo::IptcCoreRightsUsageTerms: return fromXmpLangAlt("Xmp.xmpRights.UsageTerms"); case MetadataInfo::IptcCoreSource: return fromIptcOrXmp("Iptc.Application2.Source", "Xmp.photoshop.Source"); case MetadataInfo::IptcCoreCreatorJobTitle: return fromIptcOrXmp("Iptc.Application2.BylineTitle", "Xmp.photoshop.AuthorsPosition"); case MetadataInfo::IptcCoreInstructions: return fromIptcOrXmp("Iptc.Application2.SpecialInstructions", "Xmp.photoshop.Instructions"); case MetadataInfo::IptcCoreLocationInfo: { IptcCoreLocationInfo location = getIptcCoreLocation(); if (location.isNull()) { return QVariant(); } return QVariant::fromValue(location); } case MetadataInfo::IptcCoreCountryCode: return fromIptcOrXmp("Iptc.Application2.CountryCode", "Xmp.iptc.CountryCode"); case MetadataInfo::IptcCoreCountry: return fromIptcOrXmp("Iptc.Application2.CountryName", "Xmp.photoshop.Country"); case MetadataInfo::IptcCoreCity: return fromIptcOrXmp("Iptc.Application2.City", "Xmp.photoshop.City"); case MetadataInfo::IptcCoreLocation: return fromIptcOrXmp("Iptc.Application2.SubLocation", "Xmp.iptc.Location"); case MetadataInfo::IptcCoreProvinceState: return fromIptcOrXmp("Iptc.Application2.ProvinceState", "Xmp.photoshop.State"); case MetadataInfo::IptcCoreIntellectualGenre: return fromIptcOrXmp("Iptc.Application2.ObjectAttribute", "Xmp.iptc.IntellectualGenre"); case MetadataInfo::IptcCoreJobID: return fromIptcOrXmp("Iptc.Application2.TransmissionReference", "Xmp.photoshop.TransmissionReference"); case MetadataInfo::IptcCoreScene: return fromXmpList("Xmp.iptc.Scene"); case MetadataInfo::IptcCoreSubjectCode: return toStringListVariant(getIptcCoreSubjects()); case MetadataInfo::IptcCoreContactInfo: { IptcCoreContactInfo info = getCreatorContactInfo(); if (info.isNull()) { return QVariant(); } return QVariant::fromValue(info); } case MetadataInfo::IptcCoreContactInfoCity: return getXmpTagVariant("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCity"); case MetadataInfo::IptcCoreContactInfoCountry: return getXmpTagVariant("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCtry"); case MetadataInfo::IptcCoreContactInfoAddress: return getXmpTagVariant("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrExtadr"); case MetadataInfo::IptcCoreContactInfoPostalCode: return getXmpTagVariant("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrPcode"); case MetadataInfo::IptcCoreContactInfoProvinceState: return getXmpTagVariant("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrRegion"); case MetadataInfo::IptcCoreContactInfoEmail: return getXmpTagVariant("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiEmailWork"); case MetadataInfo::IptcCoreContactInfoPhone: return getXmpTagVariant("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiTelWork"); case MetadataInfo::IptcCoreContactInfoWebUrl: return getXmpTagVariant("Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiUrlWork"); case MetadataInfo::AspectRatio: return fromXmpLangAlt("Xmp.video.AspectRatio"); case MetadataInfo::AudioBitRate: return fromXmpLangAlt("Xmp.audio.SampleRate"); case MetadataInfo::AudioChannelType: return fromXmpLangAlt("Xmp.audio.ChannelType"); case MetadataInfo::AudioCodec: return fromXmpLangAlt("Xmp.audio.Codec"); case MetadataInfo::Duration: return fromXmpLangAlt("Xmp.video.Duration"); // duration is in ms case MetadataInfo::FrameRate: return fromXmpLangAlt("Xmp.video.FrameRate"); case MetadataInfo::VideoCodec: return fromXmpLangAlt("Xmp.video.Codec"); case MetadataInfo::VideoBitDepth: return fromXmpLangAlt("Xmp.video.BitDepth"); case MetadataInfo::VideoHeight: return fromXmpLangAlt("Xmp.video.Height"); case MetadataInfo::VideoWidth: return fromXmpLangAlt("Xmp.video.Width"); + case MetadataInfo::VideoColorSpace: + return fromXmpLangAlt("Xmp.video.ColorMode"); default: return QVariant(); } } QVariantList DMetadata::getMetadataFields(const MetadataFields& fields) const { QVariantList list; foreach(MetadataInfo::Field field, fields) // krazy:exclude=foreach { list << getMetadataField(field); } return list; } QString DMetadata::valueToString (const QVariant& value, MetadataInfo::Field field) { MetaEngine exiv2Iface; switch (field) { case MetadataInfo::Rating: return value.toString(); case MetadataInfo::CreationDate: case MetadataInfo::DigitizationDate: return value.toDateTime().toString(Qt::LocaleDate); case MetadataInfo::Orientation: { switch (value.toInt()) { // Example why the English text differs from the enum names: ORIENTATION_ROT_90. // Rotation by 90 degrees is right (clockwise) rotation. // But: The enum names describe what needs to be done to get the image right again. // And an image that needs to be rotated 90 degrees is currently rotated 270 degrees = left. case ORIENTATION_UNSPECIFIED: return i18n("Unspecified"); case ORIENTATION_NORMAL: return i18nc("Rotation of an unrotated image", "Normal"); case ORIENTATION_HFLIP: return i18n("Flipped Horizontally"); case ORIENTATION_ROT_180: return i18n("Rotated by 180 Degrees"); case ORIENTATION_VFLIP: return i18n("Flipped Vertically"); case ORIENTATION_ROT_90_HFLIP: return i18n("Flipped Horizontally and Rotated Left"); case ORIENTATION_ROT_90: return i18n("Rotated Left"); case ORIENTATION_ROT_90_VFLIP: return i18n("Flipped Vertically and Rotated Left"); case ORIENTATION_ROT_270: return i18n("Rotated Right"); default: return i18n("Unknown"); } break; } case MetadataInfo::Make: return exiv2Iface.createExifUserStringFromValue("Exif.Image.Make", value); case MetadataInfo::Model: return exiv2Iface.createExifUserStringFromValue("Exif.Image.Model", value); case MetadataInfo::Lens: // heterogeneous source, non-standardized string return value.toString(); case MetadataInfo::Aperture: return exiv2Iface.createExifUserStringFromValue("Exif.Photo.FNumber", value); case MetadataInfo::FocalLength: return exiv2Iface.createExifUserStringFromValue("Exif.Photo.FocalLength", value); case MetadataInfo::FocalLengthIn35mm: return exiv2Iface.createExifUserStringFromValue("Exif.Photo.FocalLengthIn35mmFilm", value); case MetadataInfo::ExposureTime: return exiv2Iface.createExifUserStringFromValue("Exif.Photo.ExposureTime", value); case MetadataInfo::ExposureProgram: return exiv2Iface.createExifUserStringFromValue("Exif.Photo.ExposureProgram", value); case MetadataInfo::ExposureMode: return exiv2Iface.createExifUserStringFromValue("Exif.Photo.ExposureMode", value); case MetadataInfo::Sensitivity: return exiv2Iface.createExifUserStringFromValue("Exif.Photo.ISOSpeedRatings", value); case MetadataInfo::FlashMode: return exiv2Iface.createExifUserStringFromValue("Exif.Photo.Flash", value); case MetadataInfo::WhiteBalance: return exiv2Iface.createExifUserStringFromValue("Exif.Photo.WhiteBalance", value); case MetadataInfo::MeteringMode: return exiv2Iface.createExifUserStringFromValue("Exif.Photo.MeteringMode", value); case MetadataInfo::SubjectDistance: return exiv2Iface.createExifUserStringFromValue("Exif.Photo.SubjectDistance", value); case MetadataInfo::SubjectDistanceCategory: return exiv2Iface.createExifUserStringFromValue("Exif.Photo.SubjectDistanceRange", value); case MetadataInfo::WhiteBalanceColorTemperature: return i18nc("Temperature in Kelvin", "%1 K", value.toInt()); case MetadataInfo::AspectRatio: case MetadataInfo::AudioBitRate: case MetadataInfo::AudioChannelType: case MetadataInfo::AudioCodec: case MetadataInfo::Duration: case MetadataInfo::FrameRate: case MetadataInfo::VideoCodec: return value.toString(); case MetadataInfo::Longitude: { int degrees, minutes; double seconds; char directionRef; if (!convertToUserPresentableNumbers(value.toString(), °rees, &minutes, &seconds, &directionRef)) { return QString(); } QString direction = (QLatin1Char(directionRef) == QLatin1Char('W')) ? i18nc("For use in longitude coordinate", "West") : i18nc("For use in longitude coordinate", "East"); return QString::fromLatin1("%1%2%3%4%L5%6 %7").arg(degrees).arg(QChar(0xB0)) .arg(minutes).arg(QChar(0x2032)) .arg(seconds, 'f').arg(QChar(0x2033)).arg(direction); } case MetadataInfo::LongitudeNumber: { int degrees, minutes; double seconds; char directionRef; convertToUserPresentableNumbers(false, value.toDouble(), °rees, &minutes, &seconds, &directionRef); QString direction = (QLatin1Char(directionRef) == QLatin1Char('W')) ? i18nc("For use in longitude coordinate", "West") : i18nc("For use in longitude coordinate", "East"); return QString::fromLatin1("%1%2%3%4%L5%6 %7").arg(degrees).arg(QChar(0xB0)) .arg(minutes).arg(QChar(0x2032)) .arg(seconds, 'f').arg(QChar(0x2033)).arg(direction); } case MetadataInfo::Latitude: { int degrees, minutes; double seconds; char directionRef; if (!convertToUserPresentableNumbers(value.toString(), °rees, &minutes, &seconds, &directionRef)) { return QString(); } QString direction = (QLatin1Char(directionRef) == QLatin1Char('N')) ? i18nc("For use in latitude coordinate", "North") : i18nc("For use in latitude coordinate", "South"); return QString::fromLatin1("%1%2%3%4%L5%6 %7").arg(degrees).arg(QChar(0xB0)) .arg(minutes).arg(QChar(0x2032)) .arg(seconds, 'f').arg(QChar(0x2033)).arg(direction); } case MetadataInfo::LatitudeNumber: { int degrees, minutes; double seconds; char directionRef; convertToUserPresentableNumbers(false, value.toDouble(), °rees, &minutes, &seconds, &directionRef); QString direction = (QLatin1Char(directionRef) == QLatin1Char('N')) ? i18nc("For use in latitude coordinate", "North") : i18nc("For use in latitude coordinate", "South"); return QString::fromLatin1("%1%2%3%4%L5%6 %7").arg(degrees).arg(QChar(0xB0)) .arg(minutes).arg(QChar(0x2032)) .arg(seconds, 'f').arg(QChar(0x2033)).arg(direction); } case MetadataInfo::Altitude: { QString meters = QString::fromLatin1("%L1").arg(value.toDouble(), 0, 'f', 2); // xgettext: no-c-format return i18nc("Height in meters", "%L1m", meters); // krazy:exclude=i18ncheckarg } case MetadataInfo::PositionOrientation: case MetadataInfo::PositionTilt: case MetadataInfo::PositionRoll: case MetadataInfo::PositionAccuracy: //TODO return value.toString(); case MetadataInfo::PositionDescription: return value.toString(); // Lang Alt case MetadataInfo::IptcCoreCopyrightNotice: case MetadataInfo::IptcCoreRightsUsageTerms: case MetadataInfo::Description: case MetadataInfo::Title: { QMap map = value.toMap(); // the most common cases if (map.isEmpty()) { return QString(); } else if (map.size() == 1) { return map.begin().value().toString(); } // Try "en-us" QString spec = QLocale().name().toLower().replace(QLatin1Char('_'), QLatin1Char('-')); if (map.contains(spec)) { return map[spec].toString(); } // Try "en-" QStringList keys = map.keys(); QString spec2 = QLocale().name().toLower(); QRegExp exp(spec2.left(spec2.indexOf(QLatin1Char('_'))) + QLatin1Char('-')); QStringList matches = keys.filter(exp); if (!matches.isEmpty()) { return map[matches.first()].toString(); } // return default if (map.contains(QLatin1String("x-default"))) { return map[QLatin1String("x-default")].toString(); } // return first entry return map.begin().value().toString(); } // List case MetadataInfo::IptcCoreCreator: case MetadataInfo::IptcCoreScene: case MetadataInfo::IptcCoreSubjectCode: return value.toStringList().join(QLatin1String(" ")); // Text case MetadataInfo::Comment: case MetadataInfo::CommentJfif: case MetadataInfo::CommentExif: case MetadataInfo::CommentIptc: case MetadataInfo::Headline: case MetadataInfo::DescriptionWriter: case MetadataInfo::IptcCoreProvider: case MetadataInfo::IptcCoreSource: case MetadataInfo::IptcCoreCreatorJobTitle: case MetadataInfo::IptcCoreInstructions: case MetadataInfo::IptcCoreCountryCode: case MetadataInfo::IptcCoreCountry: case MetadataInfo::IptcCoreCity: case MetadataInfo::IptcCoreLocation: case MetadataInfo::IptcCoreProvinceState: case MetadataInfo::IptcCoreIntellectualGenre: case MetadataInfo::IptcCoreJobID: return value.toString(); default: break; } return QString(); } QStringList DMetadata::valuesToString(const QVariantList& values, const MetadataFields& fields) { int size = values.size(); Q_ASSERT(size == values.size()); QStringList list; for (int i = 0; i < size; ++i) { list << valueToString(values.at(i), fields.at(i)); } return list; } QMap DMetadata::possibleValuesForEnumField(MetadataInfo::Field field) { QMap map; int min, max; switch (field) { case MetadataInfo::Orientation: /// Int, enum from libMetaEngine min = ORIENTATION_UNSPECIFIED; max = ORIENTATION_ROT_270; break; case MetadataInfo::ExposureProgram: /// Int, enum from Exif min = 0; max = 8; break; case MetadataInfo::ExposureMode: /// Int, enum from Exif min = 0; max = 2; break; case MetadataInfo::WhiteBalance: /// Int, enum from Exif min = 0; max = 1; break; case MetadataInfo::MeteringMode: /// Int, enum from Exif min = 0; max = 6; map[255] = valueToString(255, field); break; case MetadataInfo::SubjectDistanceCategory: /// int, enum from Exif min = 0; max = 3; break; case MetadataInfo::FlashMode: /// Int, bit mask from Exif // This one is a bit special. // We return a bit mask for binary AND searching. map[0x1] = i18n("Flash has been fired"); map[0x40] = i18n("Flash with red-eye reduction mode"); //more: TODO? return map; default: qCWarning(DIGIKAM_METAENGINE_LOG) << "Unsupported field " << field << " in DMetadata::possibleValuesForEnumField"; return map; } for (int i = min; i <= max; ++i) { map[i] = valueToString(i, field); } return map; } double DMetadata::apexApertureToFNumber(double aperture) { // convert from APEX. See Exif spec, Annex C. if (aperture == 0.0) { return 1; } else if (aperture == 1.0) { return 1.4; } else if (aperture == 2.0) { return 2; } else if (aperture == 3.0) { return 2.8; } else if (aperture == 4.0) { return 4; } else if (aperture == 5.0) { return 5.6; } else if (aperture == 6.0) { return 8; } else if (aperture == 7.0) { return 11; } else if (aperture == 8.0) { return 16; } else if (aperture == 9.0) { return 22; } else if (aperture == 10.0) { return 32; } return exp(log(2) * aperture / 2.0); } double DMetadata::apexShutterSpeedToExposureTime(double shutterSpeed) { // convert from APEX. See Exif spec, Annex C. if (shutterSpeed == -5.0) { return 30; } else if (shutterSpeed == -4.0) { return 15; } else if (shutterSpeed == -3.0) { return 8; } else if (shutterSpeed == -2.0) { return 4; } else if (shutterSpeed == -1.0) { return 2; } else if (shutterSpeed == 0.0) { return 1; } else if (shutterSpeed == 1.0) { return 0.5; } else if (shutterSpeed == 2.0) { return 0.25; } else if (shutterSpeed == 3.0) { return 0.125; } else if (shutterSpeed == 4.0) { return 1.0 / 15.0; } else if (shutterSpeed == 5.0) { return 1.0 / 30.0; } else if (shutterSpeed == 6.0) { return 1.0 / 60.0; } else if (shutterSpeed == 7.0) { return 0.008; // 1/125 } else if (shutterSpeed == 8.0) { return 0.004; // 1/250 } else if (shutterSpeed == 9.0) { return 0.002; // 1/500 } else if (shutterSpeed == 10.0) { return 0.001; // 1/1000 } else if (shutterSpeed == 11.0) { return 0.0005; // 1/2000 } // additions by me else if (shutterSpeed == 12.0) { return 0.00025; // 1/4000 } else if (shutterSpeed == 13.0) { return 0.000125; // 1/8000 } return exp( - log(2) * shutterSpeed); } MetaEngine::AltLangMap DMetadata::toAltLangMap(const QVariant& var) { MetaEngine::AltLangMap map; if (var.isNull()) { return map; } switch (var.type()) { case QVariant::String: map.insert(QLatin1String("x-default"), var.toString()); break; case QVariant::Map: { QMap varMap = var.toMap(); for (QMap::const_iterator it = varMap.constBegin(); it != varMap.constEnd(); ++it) { map.insert(it.key(), it.value().toString()); } break; } default: break; } return map; } bool DMetadata::addToXmpTagStringBag(const char* const xmpTagName, const QStringList& entriesToAdd) const { //#ifdef _XMP_SUPPORT_ QStringList oldEntries = getXmpTagStringBag(xmpTagName, false); QStringList newEntries = entriesToAdd; // Create a list of keywords including old one which already exists. for (QStringList::const_iterator it = oldEntries.constBegin(); it != oldEntries.constEnd(); ++it ) { if (!newEntries.contains(*it)) { newEntries.append(*it); } } if (setXmpTagStringBag(xmpTagName, newEntries)) { return true; } //#endif // _XMP_SUPPORT_ return false; } bool DMetadata::removeFromXmpTagStringBag(const char* const xmpTagName, const QStringList& entriesToRemove) const { //#ifdef _XMP_SUPPORT_ QStringList currentEntries = getXmpTagStringBag(xmpTagName, false); QStringList newEntries; // Create a list of current keywords except those that shall be removed for (QStringList::const_iterator it = currentEntries.constBegin(); it != currentEntries.constEnd(); ++it ) { if (!entriesToRemove.contains(*it)) { newEntries.append(*it); } } if (setXmpTagStringBag(xmpTagName, newEntries)) { return true; } //#endif // _XMP_SUPPORT_ return false; } QStringList DMetadata::getXmpKeywords() const { return (getXmpTagStringBag("Xmp.dc.subject", false)); } bool DMetadata::setXmpKeywords(const QStringList& newKeywords) const { return setXmpTagStringBag("Xmp.dc.subject", newKeywords); } bool DMetadata::removeXmpKeywords(const QStringList& keywordsToRemove) { return removeFromXmpTagStringBag("Xmp.dc.subject", keywordsToRemove); } QStringList DMetadata::getXmpSubCategories() const { return (getXmpTagStringBag("Xmp.photoshop.SupplementalCategories", false)); } bool DMetadata::setXmpSubCategories(const QStringList& newSubCategories) const { return addToXmpTagStringBag("Xmp.photoshop.SupplementalCategories", newSubCategories); } bool DMetadata::removeXmpSubCategories(const QStringList& subCategoriesToRemove) { return removeFromXmpTagStringBag("Xmp.photoshop.SupplementalCategories", subCategoriesToRemove); } QStringList DMetadata::getXmpSubjects() const { return (getXmpTagStringBag("Xmp.iptc.SubjectCode", false)); } bool DMetadata::setXmpSubjects(const QStringList& newSubjects) const { return addToXmpTagStringBag("Xmp.iptc.SubjectCode", newSubjects); } bool DMetadata::removeXmpSubjects(const QStringList& subjectsToRemove) { return removeFromXmpTagStringBag("Xmp.iptc.SubjectCode", subjectsToRemove); } bool DMetadata::removeExifColorSpace() const { bool ret = true; ret &= removeExifTag("Exif.Photo.ColorSpace"); ret &= removeXmpTag("Xmp.exif.ColorSpace"); return ret; } QString DMetadata::getExifTagStringFromTagsList(const QStringList& tagsList) const { QString val; foreach(const QString& tag, tagsList) { val = getExifTagString(tag.toLatin1().constData()); if (!val.isEmpty()) return val; } return QString(); } bool DMetadata::removeExifTags(const QStringList& tagFilters) { MetaDataMap m = getExifTagsDataList(tagFilters); if (m.isEmpty()) return false; for (MetaDataMap::iterator it = m.begin() ; it != m.end() ; ++it) { removeExifTag(it.key().toLatin1().constData()); } return true; } bool DMetadata::removeIptcTags(const QStringList& tagFilters) { MetaDataMap m = getIptcTagsDataList(tagFilters); if (m.isEmpty()) return false; for (MetaDataMap::iterator it = m.begin() ; it != m.end() ; ++it) { removeIptcTag(it.key().toLatin1().constData()); } return true; } bool DMetadata::removeXmpTags(const QStringList& tagFilters) { MetaDataMap m = getXmpTagsDataList(tagFilters); if (m.isEmpty()) return false; for (MetaDataMap::iterator it = m.begin() ; it != m.end() ; ++it) { removeXmpTag(it.key().toLatin1().constData()); } return true; } } // namespace Digikam diff --git a/libs/dmetadata/dmetadata.h b/libs/dmetadata/dmetadata.h index 1155eb26c1..f1d80f4b5e 100644 --- a/libs/dmetadata/dmetadata.h +++ b/libs/dmetadata/dmetadata.h @@ -1,283 +1,287 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2006-02-23 * Description : image metadata interface * * Copyright (C) 2006-2018 by Gilles Caulier * Copyright (C) 2006-2013 by Marcel Wiesweg * * 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. * * ============================================================ */ #ifndef DMETADATA_H #define DMETADATA_H // Qt includes #include #include // Local includes #include "metaengine.h" #include "metaengine_data.h" #include "captionvalues.h" #include "metadatasettingscontainer.h" #include "infocontainer.h" #include "metadatainfo.h" #include "digikam_export.h" #include "dmetadatasettings.h" namespace Digikam { class Template; class IccProfile; class DIGIKAM_EXPORT DMetadata : public MetaEngine { public: DMetadata(); explicit DMetadata(const QString& filePath); explicit DMetadata(const MetaEngineData& data); ~DMetadata(); void registerMetadataSettings(); void setSettings(const MetadataSettingsContainer& settings); /** Re-implemented from libMetaEngine to use libraw identify and * ffmpeg probe methods if Exiv2 failed. */ bool load(const QString& filePath); bool save(const QString& filePath) const; bool applyChanges() const; /** Try to extract metadata using Raw Engine identify method (libraw) */ bool loadUsingRawEngine(const QString& filePath); /** Try to extract metadata using FFMpeg probe method (libav) */ bool loadUsingFFmpeg(const QString& filePath); /** Metadata manipulation methods */ CaptionsMap getImageComments(const DMetadataSettingsContainer& settings = DMetadataSettings::instance()->settings()) const; bool setImageComments(const CaptionsMap& comments, const DMetadataSettingsContainer& settings = DMetadataSettings::instance()->settings()) const; int getImagePickLabel() const; bool setImagePickLabel(int pickId) const; int getImageColorLabel() const; bool setImageColorLabel(int colorId) const; CaptionsMap getImageTitles() const; bool setImageTitles(const CaptionsMap& title) const; int getImageRating(const DMetadataSettingsContainer& settings = DMetadataSettings::instance()->settings()) const; bool setImageRating(int rating, const DMetadataSettingsContainer& settings = DMetadataSettings::instance()->settings()) const; bool getImageTagsPath(QStringList& tagsPath, const DMetadataSettingsContainer& settings = DMetadataSettings::instance()->settings()) const; bool setImageTagsPath(const QStringList& tagsPath, const DMetadataSettingsContainer& settings = DMetadataSettings::instance()->settings()) const; bool getACDSeeTagsPath(QStringList& tagsPath) const; bool setACDSeeTagsPath(const QStringList& tagsPath) const; /** Get Images Face Map based on tags stored in Picassa/Metadatagroup * format. Use $ exiv2 -pa image to see the tag structure */ bool getImageFacesMap(QMultiMap& facesPath) const; /** Set Images Face Map tags in Picassa/Metadatagroup format * Use exiv2 -pa image to check for face tags, * @param write - if true all faces will be written, else update mode: * search if at least a face tag exist and write if true */ bool setImageFacesMap(QMultiMap& facesPath, bool write) const; bool setMetadataTemplate(const Template& t) const; Template getMetadataTemplate() const; bool removeMetadataTemplate() const; QString getImageHistory() const; bool setImageHistory(QString& imageHistoryXml) const; bool hasImageHistoryTag() const; QString getImageUniqueId() const; bool setImageUniqueId(const QString& uuid) const; /// Fills only the copyright values in the template. Use getMetadataTemplate() usually. /// Returns true if valid fields were read. bool getCopyrightInformation(Template& t) const; IptcCoreContactInfo getCreatorContactInfo() const; bool setCreatorContactInfo(const IptcCoreContactInfo& info) const; IptcCoreLocationInfo getIptcCoreLocation() const; bool setIptcCoreLocation(const IptcCoreLocationInfo& location) const; QStringList getIptcCoreSubjects() const; /** Return a string with Lens mounted on the front of camera. There no standard Exif tag for Lens information. Camera makernotes and Xmp tags are parsed. Take a care : lens information are not standardized and string content is not homogeneous between camera model/maker. */ QString getLensDescription() const; /** Reads an IccProfile that is described or embedded in the metadata. This method does not retrieve profiles embedded in the image but not the metadata, e.g. embedded profiles in JPEG images. Returns a null profile if no profile is found. */ IccProfile getIccProfile() const; /** Sets the IccProfile embedded in the Exif metadata. */ bool setIccProfile(const IccProfile& profile); /** Remove the Exif color space identification from the image */ bool removeExifColorSpace() const; PhotoInfoContainer getPhotographInformation() const; /** Returns video metadata from Xmp tags. */ VideoInfoContainer getVideoInformation() const; /** Returns millisecond time-stamp from Exif tags or 0 if not found. */ int getMSecsInfo() const; /** Extract milliseconds time-stamp of photo from an Exif tag and store it to 'ms'. * Returns true if data are extracted. */ bool mSecTimeStamp(const char* const exifTagName, int& ms) const; /** Returns the requested metadata field as a QVariant. See metadatainfo.h for a specification of the format of the QVariant. */ QVariant getMetadataField(MetadataInfo::Field field) const; QVariantList getMetadataFields(const MetadataFields& fields) const; /** Convert a QVariant value of the specified field to a user-presentable, i18n'ed string. The QVariant must be of the type as specified in metadatainfo.h and as obtained by getMetadataField. */ static QString valueToString (const QVariant& value, MetadataInfo::Field field); static QStringList valuesToString(const QVariantList& list, const MetadataFields& fields); /** Returns a map of possible enum values and their user-presentable, i18n'ed representation. Valid fields are those which are described as "enum from" or "bit mask from" in metadatainfo.h. */ static QMap possibleValuesForEnumField(MetadataInfo::Field field); static double apexApertureToFNumber(double aperture); static double apexShutterSpeedToExposureTime(double shutterSpeed); static MetaEngine::AltLangMap toAltLangMap(const QVariant& var); // These methods have been factored to libMetaEngine 2.3.0. Remove it after KDE 4.8.2 /** Set an Xmp tag content using a list of strings defined by the 'entriesToAdd' parameter. The existing entries are preserved. The method will compare all new with all already existing entries to prevent duplicates in the image. Return true if the entries have been added to metadata. */ bool addToXmpTagStringBag(const char* const xmpTagName, const QStringList& entriesToAdd) const; /** Remove those Xmp tag entries that are listed in entriesToRemove from the entries in metadata. Return true if tag entries are no longer contained in metadata. All other entries are preserved. */ bool removeFromXmpTagStringBag(const char* const xmpTagName, const QStringList& entriesToRemove) const; /** Return a strings list of Xmp keywords from image. Return an empty list if no keyword are set. */ QStringList getXmpKeywords() const; /** Set Xmp keywords using a list of strings defined by 'newKeywords' parameter. The existing keywords from image are preserved. The method will compare all new keywords with all already existing keywords to prevent duplicate entries in image. Return true if keywords have been changed in metadata. */ bool setXmpKeywords(const QStringList& newKeywords) const; /** Remove those Xmp keywords that are listed in keywordsToRemove from the keywords in metadata. Return true if keywords are no longer contained in metadata. */ bool removeXmpKeywords(const QStringList& keywordsToRemove); /** Return a strings list of Xmp subjects from image. Return an empty list if no subject are set. */ QStringList getXmpSubjects() const; /** Set Xmp subjects using a list of strings defined by 'newSubjects' parameter. The existing subjects from image are preserved. The method will compare all new subject with all already existing subject to prevent duplicate entries in image. Return true if subjects have been changed in metadata. */ bool setXmpSubjects(const QStringList& newSubjects) const; /** Remove those Xmp subjects that are listed in subjectsToRemove from the subjects in metadata. Return true if subjects are no longer contained in metadata. */ bool removeXmpSubjects(const QStringList& subjectsToRemove); /** Return a strings list of Xmp sub-categories from image. Return an empty list if no sub-category are set. */ QStringList getXmpSubCategories() const; /** Set Xmp sub-categories using a list of strings defined by 'newSubCategories' parameter. The existing sub-categories from image are preserved. The method will compare all new sub-categories with all already existing sub-categories to prevent duplicate entries in image. Return true if sub-categories have been changed in metadata. */ bool setXmpSubCategories(const QStringList& newSubCategories) const; /** Remove those Xmp sub-categories that are listed in categoriesToRemove from the sub-categories in metadata. Return true if subjects are no longer contained in metadata. */ bool removeXmpSubCategories(const QStringList& categoriesToRemove); bool removeExifTags(const QStringList& tagFilters); bool removeIptcTags(const QStringList& tagFilters); bool removeXmpTags(const QStringList& tagFilters); + /** Return the description of video color space detected by FFMpeg + */ + static QString videoColorModelToString(int colorSpace); + private: bool setIptcTag(const QString& text, int maxLength, const char* const debugLabel, const char* const tagKey) const; QVariant fromExifOrXmp(const char* const exifTagName, const char* const xmpTagName) const; QVariant fromIptcOrXmp(const char* const iptcTagName, const char* const xmpTagName) const; QVariant fromXmpList(const char* const xmpTagName) const; QVariant fromIptcEmulateList(const char* const iptcTagName) const; QVariant fromXmpLangAlt(const char* const xmpTagName) const; QVariant fromIptcEmulateLangAlt(const char* const iptcTagName) const; QVariant toStringListVariant(const QStringList& list) const; QString getExifTagStringFromTagsList(const QStringList& tagsList) const; }; } // namespace Digikam #endif // DMETADATA_H diff --git a/libs/dmetadata/dmetadata_ffmpeg.cpp b/libs/dmetadata/dmetadata_ffmpeg.cpp index 2cf87da754..adf91ed743 100644 --- a/libs/dmetadata/dmetadata_ffmpeg.cpp +++ b/libs/dmetadata/dmetadata_ffmpeg.cpp @@ -1,895 +1,912 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2018-02-26 * Description : metadata extraction with ffmpeg (libav) * * References : * * FFMpeg metadata review: https://wiki.multimedia.cx/index.php?title=FFmpeg_Metadata * FFMpeg MP4 parser : https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/mov.c#L298 * Exiv2 XMP video : https://github.com/Exiv2/exiv2/blob/master/src/properties.cpp#L1331 * Apple metadata desc : https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html * Matroska metadata desc: https://matroska.org/technical/specs/tagging/index.html * FFMpeg metadata writer: https://github.com/kritzikratzi/ofxAvCodec/blob/master/src/ofxAvUtils.cpp#L61 * * Copyright (C) 2018 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. * * ============================================================ */ #include "dmetadata.h" // C Ansi includes #include // Qt includes #include #include #include #include #include #include +// KDE includes + +#include + // Local incudes #include "captionvalues.h" #include "digikam_debug.h" #include "digikam_config.h" #ifdef HAVE_MEDIAPLAYER // Libav includes extern "C" { #include #include #include #include } #endif namespace Digikam { /** Search first occurence of string in 'map' with keys given by 'lst'. * Return the string match. * If 'xmpTag' is not null, register XMP tag value with string. */ QString s_setXmpTagStringFromEntry(DMetadata* const meta, const QStringList& lst, const DMetadata::MetaDataMap& map, const char* const xmpTag=0) { foreach (QString tag, lst) { DMetadata::MetaDataMap::const_iterator it = map.find(tag); if (it != map.end()) { if (meta && // Protection. xmpTag && // If xmp tag is null, we only return the matching value from the map. meta->getXmpTagString(xmpTag).isNull()) // Only register the tag value if it doesn't exists yet. { meta->setXmpTagString(xmpTag, it.value()); } return it.value(); } } return QString(); } QStringList s_keywordsSeparation(const QString& data) { QStringList keywords = data.split(QLatin1String("/")); if (keywords.isEmpty()) { keywords = data.split(QLatin1String(",")); if (keywords.isEmpty()) { keywords = data.split(QLatin1String(" ")); } } return keywords; } qint64 s_secondsSinceJanuary1904(const QDateTime dt) { QDateTime dt1904(QDate(1904, 1, 1), QTime(0, 0, 0)); return dt1904.secsTo(dt); } #ifdef HAVE_MEDIAPLAYER DMetadata::MetaDataMap s_extractFFMpegMetadataEntriesFromDictionary(AVDictionary* const dict) { AVDictionaryEntry* entry = 0; DMetadata::MetaDataMap meta; do { entry = av_dict_get(dict, "", entry, AV_DICT_IGNORE_SUFFIX); if (entry) { meta.insert(QString::fromUtf8(entry->key), QString::fromUtf8(entry->value)); } } while (entry); return meta; } #endif bool DMetadata::loadUsingFFmpeg(const QString& filePath) { #ifdef HAVE_MEDIAPLAYER qCDebug(DIGIKAM_METAENGINE_LOG) << "Parse metadada with FFMpeg:" << filePath; av_register_all(); AVFormatContext* fmt_ctx = avformat_alloc_context(); int ret = avformat_open_input(&fmt_ctx, filePath.toUtf8().data(), NULL, NULL); if (ret < 0) { qCDebug(DIGIKAM_METAENGINE_LOG) << "avformat_open_input error: " << ret; return false; } ret = avformat_find_stream_info(fmt_ctx, NULL); if (ret < 0) { qCDebug(DIGIKAM_METAENGINE_LOG) << "avform_find_stream_info error: " << ret; return false; } QString data; QFileInfo fi(filePath); setXmpTagString("Xmp.video.FileName", fi.fileName()); setXmpTagString("Xmp.video.FileSize", QString::number(fi.size() / (1024*1024))); setXmpTagString("Xmp.video.FileType", fi.suffix()); setXmpTagString("Xmp.video.MimeType", QMimeDatabase().mimeTypeForFile(filePath).name()); setXmpTagString("Xmp.video.Duration", QString::number((int)(1000.0 * (double)fmt_ctx->duration / (double)AV_TIME_BASE))); setXmpTagString("Xmp.video.MaxBitRate", QString::number(fmt_ctx->bit_rate)); setXmpTagString("Xmp.video.StreamCount", QString::number(fmt_ctx->nb_streams)); // To only register one video, one audio stream, and one subtitle stream in XMP metadata. bool vstream = false; bool astream = false; bool sstream = false; for (uint i = 0 ; i < fmt_ctx->nb_streams ; i++) { const AVStream* const stream = fmt_ctx->streams[i]; AVCodecParameters* const codec = stream->codecpar; // ----------------------------------------- // Audio stream parsing // ----------------------------------------- if (!astream && codec->codec_type == AVMEDIA_TYPE_AUDIO) { astream = true; const char* cname = avcodec_get_name(codec->codec_id); setXmpTagString("Xmp.audio.Codec", QString::fromUtf8(cname)); setXmpTagString("Xmp.audio.CodecDescription", QString::fromUtf8(avcodec_descriptor_get_by_name(cname)->long_name)); setXmpTagString("Xmp.audio.SampleRate", QString::number(codec->sample_rate)); setXmpTagString("Xmp.audio.ChannelType", QString::number(codec->channels)); setXmpTagString("Xmp.audio.Format", QString::fromUtf8(av_get_sample_fmt_name((AVSampleFormat)codec->format))); // -------------- MetaDataMap ameta = s_extractFFMpegMetadataEntriesFromDictionary(stream->metadata); qCDebug(DIGIKAM_METAENGINE_LOG) << "-- FFMpeg audio stream metadata entries :"; qCDebug(DIGIKAM_METAENGINE_LOG) << ameta; qCDebug(DIGIKAM_METAENGINE_LOG) << "-----------------------------------------"; // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("language"), ameta, "Xmp.audio.TrackLang"); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("creation_time"), ameta); if (!data.isEmpty()) { QDateTime dt = QDateTime::fromString(data, Qt::ISODate); setXmpTagString("Xmp.audio.TrackCreateDate", QString::number(s_secondsSinceJanuary1904(dt))); } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("handler_name"), ameta, "Xmp.audio.HandlerDescription"); } // ----------------------------------------- // Video stream parsing // ----------------------------------------- if (!vstream && codec->codec_type == AVMEDIA_TYPE_VIDEO) { vstream = true; const char* cname = avcodec_get_name(codec->codec_id); setXmpTagString("Xmp.video.Codec", QString::fromUtf8(cname)); setXmpTagString("Xmp.video.CodecDescription", QString::fromUtf8(avcodec_descriptor_get_by_name(cname)->long_name)); setXmpTagString("Xmp.video.Format", QString::fromUtf8(av_get_pix_fmt_name((AVPixelFormat)codec->format))); + setXmpTagString("Xmp.video.ColorMode", + QString::number(codec->color_space)); setXmpTagString("Xmp.video.ColorSpace", - QString::fromUtf8(av_color_space_name(codec->color_space))); + videoColorModelToString(codec->color_space)); double aspectRatio = -1.0; int frameRate = -1.0; if (codec->sample_aspect_ratio.num && // Check if undefined codec->sample_aspect_ratio.den) // Check div by 0 { aspectRatio = (double)codec->sample_aspect_ratio.num / (double)codec->sample_aspect_ratio.den; } else if (codec->height) { aspectRatio = (double)codec->width / (double)codec->height; } if (stream->avg_frame_rate.den) { frameRate = (double)stream->avg_frame_rate.num / (double)stream->avg_frame_rate.den; } setXmpTagString("Xmp.video.Width", QString::number(codec->width)); setXmpTagString("Xmp.video.Height", QString::number(codec->height)); setXmpTagString("Xmp.video.FrameWidth", QString::number(codec->width)); setXmpTagString("Xmp.video.FrameHeight", QString::number(codec->height)); setXmpTagString("Xmp.video.FrameSize", QString::fromLatin1("w:%1, h:%2, unit:pixels").arg(codec->width).arg(codec->height)); setXmpTagString("Xmp.video.SourceImageWidth", QString::number(codec->width)); setXmpTagString("Xmp.video.SourceImageHeight", QString::number(codec->height)); // Backport size in Exif and Iptc setImageDimensions(QSize(codec->width, codec->height)); if (aspectRatio != -1.0) setXmpTagString("Xmp.video.AspectRatio", QString::number(aspectRatio)); if (frameRate != -1.0) setXmpTagString("Xmp.video.FrameRate", QString::number(frameRate)); setXmpTagString("Xmp.video.BitDepth", QString::number(codec->bits_per_coded_sample)); // ----------------------------------------- MetaDataMap vmeta = s_extractFFMpegMetadataEntriesFromDictionary(stream->metadata); qCDebug(DIGIKAM_METAENGINE_LOG) << "-- FFMpeg video stream metadata entries :"; qCDebug(DIGIKAM_METAENGINE_LOG) << vmeta; qCDebug(DIGIKAM_METAENGINE_LOG) << "-----------------------------------------"; // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("rotate"), vmeta); if (!data.isEmpty()) { bool b = false; int val = data.toInt(&b); ImageOrientation ori = ORIENTATION_UNSPECIFIED; if (b) { switch (val) { case 0: ori = ORIENTATION_NORMAL; break; case 90: ori = ORIENTATION_ROT_90; break; case 180: ori = ORIENTATION_ROT_180; break; case 270: ori = ORIENTATION_ROT_270; break; default: break; } setXmpTagString("Xmp.video.Orientation", QString::number(ori)); // Backport orientation in Exif setImageOrientation(ori); } } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("language"), vmeta, "Xmp.video.Language"); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("creation_time") << QLatin1String("_STATISTICS_WRITING_DATE_UTC"), // MKV vmeta); if (!data.isEmpty()) { QDateTime dt = QDateTime::fromString(data, Qt::ISODate); setXmpTagString("Xmp.video.TrackCreateDate", QString::number(s_secondsSinceJanuary1904(dt))); } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("handler_name"), vmeta, "Xmp.video.HandlerDescription"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("_STATISTICS_WRITING_APP"), // MKV (in video stream, not root container) vmeta, "Xmp.video.SoftwareVersion"); } // ----------------------------------------- // Subtitle stream parsing // ----------------------------------------- if (!sstream && codec->codec_type == AVMEDIA_TYPE_SUBTITLE) { sstream = true; const char* cname = avcodec_get_name(codec->codec_id); setXmpTagString("Xmp.video.SubTCodec", QString::fromUtf8(cname)); setXmpTagString("Xmp.video.SubTCodecInfo", QString::fromUtf8(avcodec_descriptor_get_by_name(cname)->long_name)); // ----------------------------------------- MetaDataMap smeta = s_extractFFMpegMetadataEntriesFromDictionary(stream->metadata); qCDebug(DIGIKAM_METAENGINE_LOG) << "-- FFMpeg subtitle stream metadata entries :"; qCDebug(DIGIKAM_METAENGINE_LOG) << smeta; qCDebug(DIGIKAM_METAENGINE_LOG) << "--------------------------------------------"; // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("subtitle") << QLatin1String("title"), smeta, "Xmp.video.Subtitle"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("language"), smeta, "Xmp.video.SubTLang"); } } // ----------------------------------------- // Root container parsing // ----------------------------------------- MetaDataMap rmeta = s_extractFFMpegMetadataEntriesFromDictionary(fmt_ctx->metadata); qCDebug(DIGIKAM_METAENGINE_LOG) << "-- FFMpeg root container metadata entries :"; qCDebug(DIGIKAM_METAENGINE_LOG) << rmeta; qCDebug(DIGIKAM_METAENGINE_LOG) << "------------------------------------------"; // ---------------------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("major_brand"), rmeta, "Xmp.video.MajorBrand"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("compatible_brands"), rmeta, "Xmp.video.CompatibleBrands"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("minor_version"), rmeta, "Xmp.video.MinorVersion"); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("keywords") << QLatin1String("com.apple.quicktime.keywords"), rmeta, "Xmp.video.InfoText"); if (!data.isEmpty()) { QStringList keywords = s_keywordsSeparation(data); if (!keywords.isEmpty()) { setXmpKeywords(keywords); setIptcKeywords(QStringList(), keywords); } } // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("category"), rmeta, "Xmp.video.Subject"); if (!data.isEmpty()) { QStringList categories = s_keywordsSeparation(data); if (!categories.isEmpty()) { setXmpSubCategories(categories); setIptcSubCategories(QStringList(), categories); } } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("premiere_version") << QLatin1String("quicktime_version") << QLatin1String("com.apple.quicktime.software"), rmeta, "Xmp.video.SoftwareVersion"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("firmware") << QLatin1String("com.apple.proapps.serialno"), rmeta, "Xmp.video.FirmwareVersion"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("composer"), rmeta, "Xmp.video.Composer"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("com.apple.quicktime.displayname"), rmeta, "Xmp.video.Name"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("playback_requirements"), rmeta, "Xmp.video.Requirements"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("lyrics"), rmeta, "Xmp.video.Lyrics"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("performers"), rmeta, "Xmp.video.Performers"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("producer") << QLatin1String("com.apple.quicktime.producer"), rmeta, "Xmp.video.Producer"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("artist") << QLatin1String("album_artist") << QLatin1String("original_artist") << QLatin1String("com.apple.quicktime.artist"), rmeta, "Xmp.video.Artist"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("director") << QLatin1String("com.apple.quicktime.director"), rmeta, "Xmp.video.Director"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("media_type"), rmeta, "Xmp.video.Medium"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("grouping"), rmeta, "Xmp.video.Grouping"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("encoder"), rmeta, "Xmp.video.Encoder"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("com.apple.proapps.clipID"), rmeta, "Xmp.video.FileID"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("original_source") << QLatin1String("com.apple.proapps.cameraName"), rmeta, "Xmp.video.Source"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("original_format") << QLatin1String("com.apple.proapps.originalFormat"), rmeta, "Xmp.video.Format"); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("rating") << QLatin1String("com.apple.quicktime.rating.user"), rmeta, "Xmp.video.Rating"); if (!data.isEmpty()) { // Backport rating in Exif and Iptc bool b = false; int rating = data.toInt(&b); if (b) { setImageRating(rating); } } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("make") << QLatin1String("com.apple.quicktime.make"), rmeta, "Xmp.video.Make"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("model") << QLatin1String("com.apple.quicktime.model"), rmeta, "Xmp.video.Model"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("URL"), rmeta, "Xmp.video.URL"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("title") << QLatin1String("com.apple.quicktime.title"), rmeta, "Xmp.video.Title"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("author") << QLatin1String("com.apple.quicktime.author"), rmeta, "Xmp.video.Artist"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("copyright") << QLatin1String("com.apple.quicktime.copyright"), rmeta, "Xmp.video.Copyright"); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("comment") << QLatin1String("description") << QLatin1String("com.apple.quicktime.description"), rmeta, "Xmp.video.Comment"); if (!data.isEmpty()) { // Backport comment in Exif and Iptc CaptionsMap capMap; MetaEngine::AltLangMap comMap; comMap.insert(QLatin1String("x-default"), data); capMap.setData(comMap, MetaEngine::AltLangMap(), QString(), MetaEngine::AltLangMap()); setImageComments(capMap); } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("synopsis"), rmeta, "Xmp.video.Information"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("album") << QLatin1String("com.apple.quicktime.album"), rmeta, "Xmp.video.Album"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("genre") << QLatin1String("com.apple.quicktime.genre"), rmeta, "Xmp.video.Genre"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("track"), rmeta, "Xmp.video.TrackNumber"); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("year") << QLatin1String("com.apple.quicktime.year"), rmeta, "Xmp.video.Year"); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("creation_time") << QLatin1String("com.apple.quicktime.creationdate"), rmeta, "Xmp.video.DateTimeOriginal"); if (!data.isEmpty()) { setXmpTagString("Xmp.video.DateTimeDigitized", data); // Backport date in Exif and Iptc. QDateTime dt = QDateTime::fromString(data, Qt::ISODate); setImageDateTime(dt, true); } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("edit_date"), rmeta, "Xmp.video.ModificationDate"); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("date"), rmeta); if (!data.isEmpty()) { QDateTime dt = QDateTime::fromString(data, Qt::ISODate); setXmpTagString("Xmp.video.MediaCreateDate", QString::number(s_secondsSinceJanuary1904(dt))); } // -------------- // GPS info as string. ex: "+44.8511-000.6229/" // Defined in ISO 6709:2008. // Notes: altitude can be passed as 3rd values. // each value is separated from others by '-' or '+'. // '/' is always the terminaison character. data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("location") << QLatin1String("com.apple.quicktime.location.ISO6709"), rmeta, "Xmp.video.GPSCoordinates"); if (!data.isEmpty()) { // Backport location to Exif. QList digits; for (int i = 0 ; i < data.length() ; i++) { QChar c = data[i]; if (c == QLatin1Char('+') || c == QLatin1Char('-') || c == QLatin1Char('/')) { digits << i; } } QString coord; double lattitude = 0.0; double longitude = 0.0; double altitude = 0.0; bool b1 = false; bool b2 = false; bool b3 = false; if (digits.size() > 1) { coord = data.mid(digits[0], digits[1] - digits[0]); lattitude = coord.toDouble(&b1); } if (digits.size() > 2) { coord = data.mid(digits[1], digits[2] - digits[1]); longitude = coord.toDouble(&b2); } if (digits.size() > 3) { coord = data.mid(digits[2], digits[3] - digits[2]); altitude = coord.toDouble(&b3); } if (b1 && b2) { if (b3) { // All GPS values are available. setGPSInfo(altitude, lattitude, longitude); setXmpTagString("Xmp.video.GPSAltitude", getXmpTagString("Xmp.exif.GPSAltitude")); setXmpTagString("Xmp.exif.GPSAltitude", getXmpTagString("Xmp.exif.GPSAltitude")); } else { // No altitude available. double* alt = 0; setGPSInfo(alt, lattitude, longitude); } setXmpTagString("Xmp.video.GPSLatitude", getXmpTagString("Xmp.exif.GPSLatitude")); setXmpTagString("Xmp.video.GPSLongitude", getXmpTagString("Xmp.exif.GPSLongitude")); setXmpTagString("Xmp.video.GPSMapDatum", getXmpTagString("Xmp.exif.GPSMapDatum")); setXmpTagString("Xmp.video.GPSVersionID", getXmpTagString("Xmp.exif.GPSVersionID")); setXmpTagString("Xmp.exif.GPSLatitude", getXmpTagString("Xmp.exif.GPSLatitude")); setXmpTagString("Xmp.exif.GPSLongitude", getXmpTagString("Xmp.exif.GPSLongitude")); setXmpTagString("Xmp.exif.GPSMapDatum", getXmpTagString("Xmp.exif.GPSMapDatum")); setXmpTagString("Xmp.exif.GPSVersionID", getXmpTagString("Xmp.exif.GPSVersionID")); } } avformat_close_input(&fmt_ctx); return true; #else Q_UNUSED(filePath); return false; #endif } +QString DMetadata::videoColorModelToString(int colorSpace) +{ + QString cs = i18n("unknown"); + +#ifdef HAVE_MEDIAPLAYER + cs = QString::fromUtf8(av_color_space_name((AVColorSpace)colorSpace)); +#endif + + return cs; +} + } // namespace Digikam diff --git a/libs/dmetadata/metadatainfo.h b/libs/dmetadata/metadatainfo.h index 041eb46f46..0977a85bee 100644 --- a/libs/dmetadata/metadatainfo.h +++ b/libs/dmetadata/metadatainfo.h @@ -1,193 +1,194 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2007-09-12 * Description : Metadata info containers * * Copyright (C) 2007-2012 by Marcel Wiesweg * Copyright (C) 2009-2018 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. * * ============================================================ */ -#ifndef METADATAINFO_H -#define METADATAINFO_H +#ifndef METADATA_INFO_H +#define METADATA_INFO_H // Qt includes #include #include // Local includes #include "digikam_export.h" namespace Digikam { class DIGIKAM_EXPORT IptcCoreLocationInfo { public: bool operator==(const IptcCoreLocationInfo& t) const; bool isEmpty() const; bool isNull() const; QString country; QString countryCode; QString provinceState; QString city; QString location; }; //! qDebug() stream operator. Writes property @a inf to the debug output in a nicely formatted way. DIGIKAM_EXPORT QDebug operator<<(QDebug dbg, const IptcCoreLocationInfo& inf); // --------------------------------------------------------------------------------------------------- class DIGIKAM_EXPORT IptcCoreContactInfo { public: bool operator==(const IptcCoreContactInfo& t) const; bool isEmpty() const; bool isNull() const; QString city; QString country; QString address; QString postalCode; QString provinceState; QString email; QString phone; QString webUrl; }; //! qDebug() stream operator. Writes property @a inf to the debug output in a nicely formatted way. DIGIKAM_EXPORT QDebug operator<<(QDebug dbg, const IptcCoreContactInfo& inf); // --------------------------------------------------------------------------------------------------- namespace MetadataInfo { enum Field { Comment, /// String (one of the following three values) CommentJfif, /// String CommentExif, /// String CommentIptc, /// String (see also IptcCoreDescription) Description, /// Map language -> String Title, /// Map language -> String Headline, /// String DescriptionWriter, /// String Keywords, /// StringList Rating, /// Int CreationDate, /// DateTime DigitizationDate, /// DateTime Orientation, /// Int, enum from libMetaEngine Make, /// String Model, /// String Lens, /// String Aperture, /// Double, FNumber FocalLength, /// Double, mm FocalLengthIn35mm, /// Double, mm ExposureTime, /// Double, s ExposureProgram, /// Int, enum from Exif ExposureMode, /// Int, enum from Exif Sensitivity, /// Int, ISO sensitivity FlashMode, /// Int, bit mask from Exif WhiteBalance, /// Int, enum from Exif WhiteBalanceColorTemperature, /// double, color temperature in K MeteringMode, /// Int, enum from Exif SubjectDistance, /// double, m SubjectDistanceCategory, /// int, enum from Exif Latitude, /// String (as XMP GPSCoordinate) LatitudeNumber, /// double, degrees Longitude, /// String (as XMP GPSCoordinate) LongitudeNumber, /// double, degrees Altitude, /// double, m PositionOrientation, /// double, ? PositionTilt, /// double, ? PositionRoll, /// double, ? PositionAccuracy, /// double, m PositionDescription, /// String IptcCoreCopyrightNotice, /// Map language -> String IptcCoreCreator, /// List of type String IptcCoreProvider, /// String IptcCoreRightsUsageTerms, /// Map language -> String IptcCoreSource, /// String IptcCoreCreatorJobTitle, /// String IptcCoreInstructions, /// String IptcCoreLocationInfo, /// object of IptcCoreLocation, including: IptcCoreCountryCode, /// String IptcCoreCountry, /// String IptcCoreCity, /// String IptcCoreLocation, /// String IptcCoreProvinceState, /// String IptcCoreIntellectualGenre, /// String IptcCoreJobID, /// String IptcCoreScene, /// List of type String IptcCoreSubjectCode, /// List of type String IptcCoreContactInfo, /// object of IptcCoreContactInfo, including: IptcCoreContactInfoCity, /// String IptcCoreContactInfoCountry, /// String IptcCoreContactInfoAddress, /// String IptcCoreContactInfoPostalCode, /// String IptcCoreContactInfoProvinceState, /// String IptcCoreContactInfoEmail, /// String IptcCoreContactInfoPhone, /// String IptcCoreContactInfoWebUrl, /// String Faces, /// QMap // Description, DescriptionWriter, Headline, Title: see above // DateCreated: see above, CreationDate // Keywords: see above, Keywords // not supported: CreatorContactInfo // Dublin Core: Description, Title, Subject (keywords) see above AspectRatio, /// String AudioBitRate, /// String AudioChannelType, /// String - AudioCodec, /// String + AudioCodec, /// String Duration, /// String FrameRate, /// String VideoCodec, /// String VideoBitDepth, /// String VideoHeight, /// String - VideoWidth /// String + VideoWidth, /// String + VideoColorSpace /// String }; } // namespace MetadataInfo typedef QList MetadataFields; } // namespace Digikam Q_DECLARE_METATYPE(Digikam::IptcCoreContactInfo) Q_DECLARE_METATYPE(Digikam::IptcCoreLocationInfo) -#endif // METADATAINFO_H +#endif // METADATA_INFO_H