// vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau 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 of the License, 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. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include "imagemetainfomodel.h" #include "config-gwenview.h" // Qt #include #include // KF #include #include // Exiv2 #include // Local #include "gwenview_lib_debug.h" #ifdef HAVE_FITS #include "imageformats/fitsformat/fitsdata.h" #include "urlutils.h" #endif namespace Gwenview { enum GroupRow { GeneralGroup, ExifGroup, #ifdef HAVE_FITS FitsGroup, #endif IptcGroup, XmpGroup, NoGroupSpace, // second last entry NoGroup, // last entry }; class MetaInfoGroup { public: enum { InvalidRow = -1, }; class Entry { public: Entry(const QString &key, const QString &label, const QString &value) : mKey(key) , mLabel(label.trimmed()) , mValue(value.trimmed()) { } QString key() const { return mKey; } QString label() const { return mLabel; } QString value() const { return mValue; } void setValue(const QString &value) { mValue = value.trimmed(); } void appendValue(const QString &value) { if (!mValue.isEmpty()) { mValue += QLatin1Char('\n'); } mValue += value.trimmed(); } private: QString mKey; QString mLabel; QString mValue; }; MetaInfoGroup(const QString &label) : mLabel(label) { } ~MetaInfoGroup() { qDeleteAll(mList); } void clear() { qDeleteAll(mList); mList.clear(); mRowForKey.clear(); } void addEntry(const QString &key, const QString &label, const QString &value) { addEntry(new Entry(key, label, value)); } void addEntry(Entry *entry) { mList << entry; mRowForKey[entry->key()] = mList.size() - 1; } void getInfoForKey(const QString &key, QString *label, QString *value) const { Entry *entry = getEntryForKey(key); if (entry) { *label = entry->label(); *value = entry->value(); } } QString getKeyAt(int row) const { Q_ASSERT(row < mList.size()); return mList[row]->key(); } QString getLabelForKeyAt(int row) const { Q_ASSERT(row < mList.size()); return mList[row]->label(); } QString getValueForKeyAt(int row) const { Q_ASSERT(row < mList.size()); return mList[row]->value(); } void setValueForKeyAt(int row, const QString &value) { Q_ASSERT(row < mList.size()); mList[row]->setValue(value); } int getRowForKey(const QString &key) const { return mRowForKey.value(key, InvalidRow); } int size() const { return mList.size(); } QString label() const { return mLabel; } const QList &entryList() const { return mList; } private: Entry *getEntryForKey(const QString &key) const { int row = getRowForKey(key); if (row == InvalidRow) { return nullptr; } return mList[row]; } QList mList; QHash mRowForKey; QString mLabel; }; struct ImageMetaInfoModelPrivate { QVector mMetaInfoGroupVector; ImageMetaInfoModel *q; void clearGroup(MetaInfoGroup *group, const QModelIndex &parent) { if (group->size() > 0) { q->beginRemoveRows(parent, 0, group->size() - 1); group->clear(); q->endRemoveRows(); } } void setGroupEntryValue(GroupRow groupRow, const QString &key, const QString &value) { MetaInfoGroup *group = mMetaInfoGroupVector[groupRow]; const int entryRow = group->getRowForKey(key); if (entryRow == MetaInfoGroup::InvalidRow) { qCWarning(GWENVIEW_LIB_LOG) << "No row for key" << key; return; } group->setValueForKeyAt(entryRow, value); const QModelIndex groupIndex = q->index(groupRow, 0); const QModelIndex entryIndex = q->index(entryRow, 1, groupIndex); Q_EMIT q->dataChanged(entryIndex, entryIndex); } QVariant displayData(const QModelIndex &index) const { if (index.internalId() == NoGroup) { if (index.column() != 0) { return {}; } const QString label = mMetaInfoGroupVector[index.row()]->label(); return QVariant(label); } if (index.internalId() == NoGroupSpace) { return QString(); } MetaInfoGroup *group = mMetaInfoGroupVector[index.internalId()]; if (index.column() == 0) { return group->getLabelForKeyAt(index.row()); } else { return group->getValueForKeyAt(index.row()); } } void initGeneralGroup() { MetaInfoGroup *group = mMetaInfoGroupVector[GeneralGroup]; group->addEntry(QStringLiteral("General.Name"), i18nc("@item:intable Image file name", "Name"), QString()); group->addEntry(QStringLiteral("General.Size"), i18nc("@item:intable", "File Size"), QString()); group->addEntry(QStringLiteral("General.Created"), i18nc("@item:intable", "Date Created"), QString()); group->addEntry(QStringLiteral("General.Modified"), i18nc("@item:intable", "Date Modified"), QString()); group->addEntry(QStringLiteral("General.Accessed"), i18nc("@item:intable", "Date Accessed"), QString()); group->addEntry(QStringLiteral("General.LocalPath"), i18nc("@item:intable", "Path"), QString()); group->addEntry(QStringLiteral("General.ImageSize"), i18nc("@item:intable", "Image Size"), QString()); group->addEntry(QStringLiteral("General.Comment"), i18nc("@item:intable", "Comment"), QString()); group->addEntry(QStringLiteral("General.MimeType"), i18nc("@item:intable", "File Type"), QString()); } template void fillExivGroup(const QModelIndex &parent, MetaInfoGroup *group, const Container &container, const Exiv2::ExifData &exifData) { // key aren't always unique (for example, "Iptc.Application2.Keywords" // may appear multiple times) so we can't know how many rows we will // insert before going through them. That's why we create a hash // before. using EntryHash = QHash; EntryHash hash; Iterator it = container.begin(), end = container.end(); for (; it != end; ++it) { try { // Skip metadatum if its tag is an hex number if (it->tagName().substr(0, 2) == "0x") { continue; } const QString key = QString::fromUtf8(it->key().c_str()); const QString label = QString::fromLocal8Bit(it->tagLabel().c_str()); std::ostringstream stream; it->write(stream, &exifData); const QString value = QString::fromLocal8Bit(stream.str().c_str()); EntryHash::iterator hashIt = hash.find(key); if (hashIt != hash.end()) { hashIt.value()->appendValue(value); } else { hash.insert(key, new MetaInfoGroup::Entry(key, label, value)); } } catch (const std::out_of_range &error) { // Workaround for https://bugs.launchpad.net/ubuntu/+source/exiv2/+bug/1942799 // which was fixed with https://github.com/Exiv2/exiv2/pull/1918/commits/8a1e949bff482f74599f60b8ab518442036b1834 qCWarning(GWENVIEW_LIB_LOG) << "Failed to read some meta info:" << error.what(); } catch (const Exiv2::Error &error) { qCWarning(GWENVIEW_LIB_LOG) << "Failed to read some meta info:" << error.what(); } } if (hash.isEmpty()) { return; } q->beginInsertRows(parent, 0, hash.size() - 1); for (MetaInfoGroup::Entry *entry : qAsConst(hash)) { group->addEntry(entry); } q->endInsertRows(); } }; ImageMetaInfoModel::ImageMetaInfoModel() : d(new ImageMetaInfoModelPrivate) { d->q = this; #ifdef HAVE_FITS d->mMetaInfoGroupVector.resize(5); #else d->mMetaInfoGroupVector.resize(4); #endif d->mMetaInfoGroupVector[GeneralGroup] = new MetaInfoGroup(i18nc("@title:group General info about the image", "General")); d->mMetaInfoGroupVector[ExifGroup] = new MetaInfoGroup(QStringLiteral("EXIF")); #ifdef HAVE_FITS d->mMetaInfoGroupVector[FitsGroup] = new MetaInfoGroup(QStringLiteral("FITS")); #endif d->mMetaInfoGroupVector[IptcGroup] = new MetaInfoGroup(QStringLiteral("IPTC")); d->mMetaInfoGroupVector[XmpGroup] = new MetaInfoGroup(QStringLiteral("XMP")); d->initGeneralGroup(); } ImageMetaInfoModel::~ImageMetaInfoModel() { qDeleteAll(d->mMetaInfoGroupVector); delete d; } static QString formatFileTime(const KFileItem &item, const KFileItem::FileTimes timeType) { return QLocale().toString(item.time(timeType), QLocale::LongFormat); } void ImageMetaInfoModel::setDates(const QUrl &url) { KFileItem item(url); const QString modifiedString = formatFileTime(item, KFileItem::ModificationTime); const QString accessString = formatFileTime(item, KFileItem::AccessTime); const QString createdString = formatFileTime(item, KFileItem::CreationTime); d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Created"), createdString); d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Modified"), modifiedString); d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Accessed"), accessString); } void ImageMetaInfoModel::setMimeType(const QUrl &url) { KFileItem item(url); d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.MimeType"), item.mimetype()); } void ImageMetaInfoModel::setFileSize(const QUrl &url) { KFileItem item(url); d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Size"), KIO::convertSize(item.size())); } void ImageMetaInfoModel::setUrl(const QUrl &url) { KFileItem item(url); const QString localPathString = item.localPath(); d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Name"), item.name()); d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.LocalPath"), localPathString); #ifdef HAVE_FITS if (UrlUtils::urlIsFastLocalFile(url) && (url.fileName().endsWith(QLatin1String(".fit"), Qt::CaseInsensitive) || url.fileName().endsWith(QLatin1String(".fits"), Qt::CaseInsensitive))) { FITSData fitsLoader; MetaInfoGroup *group = d->mMetaInfoGroupVector[FitsGroup]; QFile file(url.toLocalFile()); if (!file.open(QIODevice::ReadOnly)) { return; } if (fitsLoader.loadFITS(file)) { QString recordList; int nkeys = 0; fitsLoader.getFITSRecord(recordList, nkeys); for (int i = 0; i < nkeys; i++) { QString record = recordList.mid(i * 80, 80); QString key; QString keyStr; QString value; if (!record.contains(QLatin1Char('='))) { key = record.section(QLatin1Char(' '), 0, 0).simplified(); keyStr = key; value = record.section(QLatin1Char(' '), 1, -1).simplified(); } else { key = record.section(QLatin1Char('='), 0, 0).simplified(); if (record.contains(QLatin1Char('/'))) { keyStr = record.section(QLatin1Char('/'), -1, -1).simplified(); value = record.section(QLatin1Char('='), 1, -1).section(QLatin1Char('/'), 0, 0); } else { keyStr = key; value = record.section(QLatin1Char('='), 1, -1); } value.remove(QStringLiteral("\'")); value = value.simplified(); } if (value.isEmpty()) { continue; } // Check if the value is a number and make it more readable bool ok = false; float number = value.toFloat(&ok); if (ok) { value = QString::number(number); } group->addEntry(QStringLiteral("Fits.") + key, keyStr, value); } } } #endif } void ImageMetaInfoModel::setImageSize(const QSize &size) { QString imageSize; if (size.isValid()) { imageSize = i18nc("@item:intable %1 is image width, %2 is image height", "%1x%2", size.width(), size.height()); double megaPixels = size.width() * size.height() / 1000000.; if (megaPixels > 0.1) { QString megaPixelsString = QString::number(megaPixels, 'f', 1); imageSize += QLatin1Char(' '); imageSize += i18nc("@item:intable %1 is number of millions of pixels in image", "(%1MP)", megaPixelsString); } } else { imageSize = QLatin1Char('-'); } d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.ImageSize"), imageSize); } void ImageMetaInfoModel::setExiv2Image(const Exiv2::Image *image) { MetaInfoGroup *exifGroup = d->mMetaInfoGroupVector[ExifGroup]; MetaInfoGroup *iptcGroup = d->mMetaInfoGroupVector[IptcGroup]; MetaInfoGroup *xmpGroup = d->mMetaInfoGroupVector[XmpGroup]; QModelIndex exifIndex = index(ExifGroup, 0); QModelIndex iptcIndex = index(IptcGroup, 0); QModelIndex xmpIndex = index(XmpGroup, 0); d->clearGroup(exifGroup, exifIndex); d->clearGroup(iptcGroup, iptcIndex); d->clearGroup(xmpGroup, xmpIndex); if (!image) { return; } d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Comment"), QString::fromUtf8(image->comment().c_str())); const Exiv2::ExifData &exifData = image->exifData(); if (image->checkMode(Exiv2::mdExif) & Exiv2::amRead) { d->fillExivGroup(exifIndex, exifGroup, exifData, exifData); } if (image->checkMode(Exiv2::mdIptc) & Exiv2::amRead) { const Exiv2::IptcData &iptcData = image->iptcData(); d->fillExivGroup(iptcIndex, iptcGroup, iptcData, exifData); } if (image->checkMode(Exiv2::mdXmp) & Exiv2::amRead) { const Exiv2::XmpData &xmpData = image->xmpData(); d->fillExivGroup(xmpIndex, xmpGroup, xmpData, exifData); } } void ImageMetaInfoModel::getInfoForKey(const QString &key, QString *label, QString *value) const { MetaInfoGroup *group; if (key.startsWith(QLatin1String("General"))) { group = d->mMetaInfoGroupVector[GeneralGroup]; } else if (key.startsWith(QLatin1String("Exif"))) { group = d->mMetaInfoGroupVector[ExifGroup]; #ifdef HAVE_FITS } else if (key.startsWith(QLatin1String("Fits"))) { group = d->mMetaInfoGroupVector[FitsGroup]; #endif } else if (key.startsWith(QLatin1String("Iptc"))) { group = d->mMetaInfoGroupVector[IptcGroup]; } else if (key.startsWith(QLatin1String("Xmp"))) { group = d->mMetaInfoGroupVector[XmpGroup]; } else { qCWarning(GWENVIEW_LIB_LOG) << "Unknown metainfo key" << key; return; } group->getInfoForKey(key, label, value); } QString ImageMetaInfoModel::getValueForKey(const QString &key) const { QString label, value; getInfoForKey(key, &label, &value); return value; } QString ImageMetaInfoModel::keyForIndex(const QModelIndex &index) const { if (index.internalId() == NoGroup) { return {}; } MetaInfoGroup *group = d->mMetaInfoGroupVector[index.internalId()]; return group->getKeyAt(index.row()); } QModelIndex ImageMetaInfoModel::index(int row, int col, const QModelIndex &parent) const { if (col < 0 || col > 1) { return {}; } if (!parent.isValid()) { // This is a group if (row < 0 || row >= d->mMetaInfoGroupVector.size()) { return {}; } return createIndex(row, col, col == 0 ? NoGroup : NoGroupSpace); } else { // This is an entry int group = parent.row(); if (row < 0 || row >= d->mMetaInfoGroupVector[group]->size()) { return {}; } return createIndex(row, col, group); } } QModelIndex ImageMetaInfoModel::parent(const QModelIndex &index) const { if (!index.isValid()) { return {}; } if (index.internalId() == NoGroup || index.internalId() == NoGroupSpace) { return {}; } else { return createIndex(index.internalId(), 0, NoGroup); } } int ImageMetaInfoModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return d->mMetaInfoGroupVector.size(); } else if (parent.internalId() == NoGroup) { return d->mMetaInfoGroupVector[parent.row()]->size(); } else { return 0; } } int ImageMetaInfoModel::columnCount(const QModelIndex & /*parent*/) const { return 2; } QVariant ImageMetaInfoModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return {}; } switch (role) { case Qt::DisplayRole: return d->displayData(index); default: return {}; } } QVariant ImageMetaInfoModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Vertical || role != Qt::DisplayRole) { return {}; } QString caption; if (section == 0) { caption = i18nc("@title:column", "Property"); } else if (section == 1) { caption = i18nc("@title:column", "Value"); } else { qCWarning(GWENVIEW_LIB_LOG) << "Unknown section" << section; } return QVariant(caption); } } // namespace #include "moc_imagemetainfomodel.cpp"