File indexing completed on 2024-05-05 08:25:40
0001 // vim: set tabstop=4 shiftwidth=4 expandtab: 0002 /* 0003 Gwenview: an image viewer 0004 Copyright 2007 Aurélien Gâteau <agateau@kde.org> 0005 0006 This program is free software; you can redistribute it and/or 0007 modify it under the terms of the GNU General Public License 0008 as published by the Free Software Foundation; either version 2 0009 of the License, or (at your option) any later version. 0010 0011 This program is distributed in the hope that it will be useful, 0012 but WITHOUT ANY WARRANTY; without even the implied warranty of 0013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0014 GNU General Public License for more details. 0015 0016 You should have received a copy of the GNU General Public License 0017 along with this program; if not, write to the Free Software 0018 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 0019 0020 */ 0021 // Self 0022 #include "imagemetainfomodel.h" 0023 #include "config-gwenview.h" 0024 0025 // Qt 0026 #include <QLocale> 0027 #include <QSize> 0028 0029 // KF 0030 #include <KFileItem> 0031 #include <KLocalizedString> 0032 0033 // Exiv2 0034 #include <exiv2/exiv2.hpp> 0035 0036 // Local 0037 #include "gwenview_lib_debug.h" 0038 #ifdef HAVE_FITS 0039 #include "imageformats/fitsformat/fitsdata.h" 0040 #include "urlutils.h" 0041 #endif 0042 0043 namespace Gwenview 0044 { 0045 enum GroupRow { 0046 GeneralGroup, 0047 ExifGroup, 0048 #ifdef HAVE_FITS 0049 FitsGroup, 0050 #endif 0051 IptcGroup, 0052 XmpGroup, 0053 NoGroupSpace, // second last entry 0054 NoGroup, // last entry 0055 }; 0056 0057 class MetaInfoGroup 0058 { 0059 public: 0060 enum { 0061 InvalidRow = -1, 0062 }; 0063 0064 class Entry 0065 { 0066 public: 0067 Entry(const QString &key, const QString &label, const QString &value) 0068 : mKey(key) 0069 , mLabel(label.trimmed()) 0070 , mValue(value.trimmed()) 0071 { 0072 } 0073 0074 QString key() const 0075 { 0076 return mKey; 0077 } 0078 QString label() const 0079 { 0080 return mLabel; 0081 } 0082 0083 QString value() const 0084 { 0085 return mValue; 0086 } 0087 void setValue(const QString &value) 0088 { 0089 mValue = value.trimmed(); 0090 } 0091 0092 void appendValue(const QString &value) 0093 { 0094 if (!mValue.isEmpty()) { 0095 mValue += QLatin1Char('\n'); 0096 } 0097 mValue += value.trimmed(); 0098 } 0099 0100 private: 0101 QString mKey; 0102 QString mLabel; 0103 QString mValue; 0104 }; 0105 0106 MetaInfoGroup(const QString &label) 0107 : mLabel(label) 0108 { 0109 } 0110 0111 ~MetaInfoGroup() 0112 { 0113 qDeleteAll(mList); 0114 } 0115 0116 void clear() 0117 { 0118 qDeleteAll(mList); 0119 mList.clear(); 0120 mRowForKey.clear(); 0121 } 0122 0123 void addEntry(const QString &key, const QString &label, const QString &value) 0124 { 0125 addEntry(new Entry(key, label, value)); 0126 } 0127 0128 void addEntry(Entry *entry) 0129 { 0130 mList << entry; 0131 mRowForKey[entry->key()] = mList.size() - 1; 0132 } 0133 0134 void getInfoForKey(const QString &key, QString *label, QString *value) const 0135 { 0136 Entry *entry = getEntryForKey(key); 0137 if (entry) { 0138 *label = entry->label(); 0139 *value = entry->value(); 0140 } 0141 } 0142 0143 QString getKeyAt(int row) const 0144 { 0145 Q_ASSERT(row < mList.size()); 0146 return mList[row]->key(); 0147 } 0148 0149 QString getLabelForKeyAt(int row) const 0150 { 0151 Q_ASSERT(row < mList.size()); 0152 return mList[row]->label(); 0153 } 0154 0155 QString getValueForKeyAt(int row) const 0156 { 0157 Q_ASSERT(row < mList.size()); 0158 return mList[row]->value(); 0159 } 0160 0161 void setValueForKeyAt(int row, const QString &value) 0162 { 0163 Q_ASSERT(row < mList.size()); 0164 mList[row]->setValue(value); 0165 } 0166 0167 int getRowForKey(const QString &key) const 0168 { 0169 return mRowForKey.value(key, InvalidRow); 0170 } 0171 0172 int size() const 0173 { 0174 return mList.size(); 0175 } 0176 0177 QString label() const 0178 { 0179 return mLabel; 0180 } 0181 0182 const QList<Entry *> &entryList() const 0183 { 0184 return mList; 0185 } 0186 0187 private: 0188 Entry *getEntryForKey(const QString &key) const 0189 { 0190 int row = getRowForKey(key); 0191 if (row == InvalidRow) { 0192 return nullptr; 0193 } 0194 return mList[row]; 0195 } 0196 0197 QList<Entry *> mList; 0198 QHash<QString, int> mRowForKey; 0199 QString mLabel; 0200 }; 0201 0202 struct ImageMetaInfoModelPrivate { 0203 QVector<MetaInfoGroup *> mMetaInfoGroupVector; 0204 ImageMetaInfoModel *q; 0205 0206 void clearGroup(MetaInfoGroup *group, const QModelIndex &parent) 0207 { 0208 if (group->size() > 0) { 0209 q->beginRemoveRows(parent, 0, group->size() - 1); 0210 group->clear(); 0211 q->endRemoveRows(); 0212 } 0213 } 0214 0215 void setGroupEntryValue(GroupRow groupRow, const QString &key, const QString &value) 0216 { 0217 MetaInfoGroup *group = mMetaInfoGroupVector[groupRow]; 0218 const int entryRow = group->getRowForKey(key); 0219 if (entryRow == MetaInfoGroup::InvalidRow) { 0220 qCWarning(GWENVIEW_LIB_LOG) << "No row for key" << key; 0221 return; 0222 } 0223 group->setValueForKeyAt(entryRow, value); 0224 const QModelIndex groupIndex = q->index(groupRow, 0); 0225 const QModelIndex entryIndex = q->index(entryRow, 1, groupIndex); 0226 Q_EMIT q->dataChanged(entryIndex, entryIndex); 0227 } 0228 0229 QVariant displayData(const QModelIndex &index) const 0230 { 0231 if (index.internalId() == NoGroup) { 0232 if (index.column() != 0) { 0233 return {}; 0234 } 0235 const QString label = mMetaInfoGroupVector[index.row()]->label(); 0236 return QVariant(label); 0237 } 0238 0239 if (index.internalId() == NoGroupSpace) { 0240 return QString(); 0241 } 0242 0243 MetaInfoGroup *group = mMetaInfoGroupVector[index.internalId()]; 0244 if (index.column() == 0) { 0245 return group->getLabelForKeyAt(index.row()); 0246 } else { 0247 return group->getValueForKeyAt(index.row()); 0248 } 0249 } 0250 0251 void initGeneralGroup() 0252 { 0253 MetaInfoGroup *group = mMetaInfoGroupVector[GeneralGroup]; 0254 group->addEntry(QStringLiteral("General.Name"), i18nc("@item:intable Image file name", "Name"), QString()); 0255 group->addEntry(QStringLiteral("General.Size"), i18nc("@item:intable", "File Size"), QString()); 0256 group->addEntry(QStringLiteral("General.Created"), i18nc("@item:intable", "Date Created"), QString()); 0257 group->addEntry(QStringLiteral("General.Modified"), i18nc("@item:intable", "Date Modified"), QString()); 0258 group->addEntry(QStringLiteral("General.Accessed"), i18nc("@item:intable", "Date Accessed"), QString()); 0259 group->addEntry(QStringLiteral("General.LocalPath"), i18nc("@item:intable", "Path"), QString()); 0260 group->addEntry(QStringLiteral("General.ImageSize"), i18nc("@item:intable", "Image Size"), QString()); 0261 group->addEntry(QStringLiteral("General.Comment"), i18nc("@item:intable", "Comment"), QString()); 0262 group->addEntry(QStringLiteral("General.MimeType"), i18nc("@item:intable", "File Type"), QString()); 0263 } 0264 0265 template<class Container, class Iterator> 0266 void fillExivGroup(const QModelIndex &parent, MetaInfoGroup *group, const Container &container, const Exiv2::ExifData &exifData) 0267 { 0268 // key aren't always unique (for example, "Iptc.Application2.Keywords" 0269 // may appear multiple times) so we can't know how many rows we will 0270 // insert before going through them. That's why we create a hash 0271 // before. 0272 using EntryHash = QHash<QString, MetaInfoGroup::Entry *>; 0273 EntryHash hash; 0274 0275 Iterator it = container.begin(), end = container.end(); 0276 0277 for (; it != end; ++it) { 0278 try { 0279 // Skip metadatum if its tag is an hex number 0280 if (it->tagName().substr(0, 2) == "0x") { 0281 continue; 0282 } 0283 const QString key = QString::fromUtf8(it->key().c_str()); 0284 const QString label = QString::fromLocal8Bit(it->tagLabel().c_str()); 0285 std::ostringstream stream; 0286 it->write(stream, &exifData); 0287 const QString value = QString::fromLocal8Bit(stream.str().c_str()); 0288 0289 EntryHash::iterator hashIt = hash.find(key); 0290 if (hashIt != hash.end()) { 0291 hashIt.value()->appendValue(value); 0292 } else { 0293 hash.insert(key, new MetaInfoGroup::Entry(key, label, value)); 0294 } 0295 } catch (const std::out_of_range &error) { 0296 // Workaround for https://bugs.launchpad.net/ubuntu/+source/exiv2/+bug/1942799 0297 // which was fixed with https://github.com/Exiv2/exiv2/pull/1918/commits/8a1e949bff482f74599f60b8ab518442036b1834 0298 qCWarning(GWENVIEW_LIB_LOG) << "Failed to read some meta info:" << error.what(); 0299 } catch (const Exiv2::Error &error) { 0300 qCWarning(GWENVIEW_LIB_LOG) << "Failed to read some meta info:" << error.what(); 0301 } 0302 } 0303 0304 if (hash.isEmpty()) { 0305 return; 0306 } 0307 q->beginInsertRows(parent, 0, hash.size() - 1); 0308 for (MetaInfoGroup::Entry *entry : qAsConst(hash)) { 0309 group->addEntry(entry); 0310 } 0311 q->endInsertRows(); 0312 } 0313 }; 0314 0315 ImageMetaInfoModel::ImageMetaInfoModel() 0316 : d(new ImageMetaInfoModelPrivate) 0317 { 0318 d->q = this; 0319 #ifdef HAVE_FITS 0320 d->mMetaInfoGroupVector.resize(5); 0321 #else 0322 d->mMetaInfoGroupVector.resize(4); 0323 #endif 0324 d->mMetaInfoGroupVector[GeneralGroup] = new MetaInfoGroup(i18nc("@title:group General info about the image", "General")); 0325 d->mMetaInfoGroupVector[ExifGroup] = new MetaInfoGroup(QStringLiteral("EXIF")); 0326 #ifdef HAVE_FITS 0327 d->mMetaInfoGroupVector[FitsGroup] = new MetaInfoGroup(QStringLiteral("FITS")); 0328 #endif 0329 d->mMetaInfoGroupVector[IptcGroup] = new MetaInfoGroup(QStringLiteral("IPTC")); 0330 d->mMetaInfoGroupVector[XmpGroup] = new MetaInfoGroup(QStringLiteral("XMP")); 0331 d->initGeneralGroup(); 0332 } 0333 0334 ImageMetaInfoModel::~ImageMetaInfoModel() 0335 { 0336 qDeleteAll(d->mMetaInfoGroupVector); 0337 delete d; 0338 } 0339 0340 static QString formatFileTime(const KFileItem &item, const KFileItem::FileTimes timeType) 0341 { 0342 return QLocale().toString(item.time(timeType), QLocale::LongFormat); 0343 } 0344 0345 void ImageMetaInfoModel::setDates(const QUrl &url) 0346 { 0347 KFileItem item(url); 0348 const QString modifiedString = formatFileTime(item, KFileItem::ModificationTime); 0349 const QString accessString = formatFileTime(item, KFileItem::AccessTime); 0350 const QString createdString = formatFileTime(item, KFileItem::CreationTime); 0351 0352 d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Created"), createdString); 0353 d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Modified"), modifiedString); 0354 d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Accessed"), accessString); 0355 } 0356 0357 void ImageMetaInfoModel::setMimeType(const QUrl &url) 0358 { 0359 KFileItem item(url); 0360 0361 d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.MimeType"), item.mimetype()); 0362 } 0363 0364 void ImageMetaInfoModel::setFileSize(const QUrl &url) 0365 { 0366 KFileItem item(url); 0367 0368 d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Size"), KIO::convertSize(item.size())); 0369 } 0370 0371 void ImageMetaInfoModel::setUrl(const QUrl &url) 0372 { 0373 KFileItem item(url); 0374 const QString localPathString = item.localPath(); 0375 0376 d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Name"), item.name()); 0377 d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.LocalPath"), localPathString); 0378 0379 #ifdef HAVE_FITS 0380 if (UrlUtils::urlIsFastLocalFile(url) 0381 && (url.fileName().endsWith(QLatin1String(".fit"), Qt::CaseInsensitive) || url.fileName().endsWith(QLatin1String(".fits"), Qt::CaseInsensitive))) { 0382 FITSData fitsLoader; 0383 MetaInfoGroup *group = d->mMetaInfoGroupVector[FitsGroup]; 0384 QFile file(url.toLocalFile()); 0385 0386 if (!file.open(QIODevice::ReadOnly)) { 0387 return; 0388 } 0389 0390 if (fitsLoader.loadFITS(file)) { 0391 QString recordList; 0392 int nkeys = 0; 0393 0394 fitsLoader.getFITSRecord(recordList, nkeys); 0395 for (int i = 0; i < nkeys; i++) { 0396 QString record = recordList.mid(i * 80, 80); 0397 QString key; 0398 QString keyStr; 0399 QString value; 0400 0401 if (!record.contains(QLatin1Char('='))) { 0402 key = record.section(QLatin1Char(' '), 0, 0).simplified(); 0403 keyStr = key; 0404 value = record.section(QLatin1Char(' '), 1, -1).simplified(); 0405 } else { 0406 key = record.section(QLatin1Char('='), 0, 0).simplified(); 0407 if (record.contains(QLatin1Char('/'))) { 0408 keyStr = record.section(QLatin1Char('/'), -1, -1).simplified(); 0409 value = record.section(QLatin1Char('='), 1, -1).section(QLatin1Char('/'), 0, 0); 0410 } else { 0411 keyStr = key; 0412 value = record.section(QLatin1Char('='), 1, -1); 0413 } 0414 value.remove(QStringLiteral("\'")); 0415 value = value.simplified(); 0416 } 0417 if (value.isEmpty()) { 0418 continue; 0419 } 0420 0421 // Check if the value is a number and make it more readable 0422 bool ok = false; 0423 float number = value.toFloat(&ok); 0424 0425 if (ok) { 0426 value = QString::number(number); 0427 } 0428 0429 group->addEntry(QStringLiteral("Fits.") + key, keyStr, value); 0430 } 0431 } 0432 } 0433 #endif 0434 } 0435 0436 void ImageMetaInfoModel::setImageSize(const QSize &size) 0437 { 0438 QString imageSize; 0439 if (size.isValid()) { 0440 imageSize = i18nc("@item:intable %1 is image width, %2 is image height", "%1x%2", size.width(), size.height()); 0441 0442 double megaPixels = size.width() * size.height() / 1000000.; 0443 if (megaPixels > 0.1) { 0444 QString megaPixelsString = QString::number(megaPixels, 'f', 1); 0445 imageSize += QLatin1Char(' '); 0446 imageSize += i18nc("@item:intable %1 is number of millions of pixels in image", "(%1MP)", megaPixelsString); 0447 } 0448 } else { 0449 imageSize = QLatin1Char('-'); 0450 } 0451 d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.ImageSize"), imageSize); 0452 } 0453 0454 void ImageMetaInfoModel::setExiv2Image(const Exiv2::Image *image) 0455 { 0456 MetaInfoGroup *exifGroup = d->mMetaInfoGroupVector[ExifGroup]; 0457 MetaInfoGroup *iptcGroup = d->mMetaInfoGroupVector[IptcGroup]; 0458 MetaInfoGroup *xmpGroup = d->mMetaInfoGroupVector[XmpGroup]; 0459 QModelIndex exifIndex = index(ExifGroup, 0); 0460 QModelIndex iptcIndex = index(IptcGroup, 0); 0461 QModelIndex xmpIndex = index(XmpGroup, 0); 0462 d->clearGroup(exifGroup, exifIndex); 0463 d->clearGroup(iptcGroup, iptcIndex); 0464 d->clearGroup(xmpGroup, xmpIndex); 0465 0466 if (!image) { 0467 return; 0468 } 0469 0470 d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Comment"), QString::fromUtf8(image->comment().c_str())); 0471 0472 const Exiv2::ExifData &exifData = image->exifData(); 0473 if (image->checkMode(Exiv2::mdExif) & Exiv2::amRead) { 0474 d->fillExivGroup<Exiv2::ExifData, Exiv2::ExifData::const_iterator>(exifIndex, exifGroup, exifData, exifData); 0475 } 0476 0477 if (image->checkMode(Exiv2::mdIptc) & Exiv2::amRead) { 0478 const Exiv2::IptcData &iptcData = image->iptcData(); 0479 d->fillExivGroup<Exiv2::IptcData, Exiv2::IptcData::const_iterator>(iptcIndex, iptcGroup, iptcData, exifData); 0480 } 0481 0482 if (image->checkMode(Exiv2::mdXmp) & Exiv2::amRead) { 0483 const Exiv2::XmpData &xmpData = image->xmpData(); 0484 d->fillExivGroup<Exiv2::XmpData, Exiv2::XmpData::const_iterator>(xmpIndex, xmpGroup, xmpData, exifData); 0485 } 0486 } 0487 0488 void ImageMetaInfoModel::getInfoForKey(const QString &key, QString *label, QString *value) const 0489 { 0490 MetaInfoGroup *group; 0491 if (key.startsWith(QLatin1String("General"))) { 0492 group = d->mMetaInfoGroupVector[GeneralGroup]; 0493 } else if (key.startsWith(QLatin1String("Exif"))) { 0494 group = d->mMetaInfoGroupVector[ExifGroup]; 0495 #ifdef HAVE_FITS 0496 } else if (key.startsWith(QLatin1String("Fits"))) { 0497 group = d->mMetaInfoGroupVector[FitsGroup]; 0498 #endif 0499 } else if (key.startsWith(QLatin1String("Iptc"))) { 0500 group = d->mMetaInfoGroupVector[IptcGroup]; 0501 } else if (key.startsWith(QLatin1String("Xmp"))) { 0502 group = d->mMetaInfoGroupVector[XmpGroup]; 0503 } else { 0504 qCWarning(GWENVIEW_LIB_LOG) << "Unknown metainfo key" << key; 0505 return; 0506 } 0507 group->getInfoForKey(key, label, value); 0508 } 0509 0510 QString ImageMetaInfoModel::getValueForKey(const QString &key) const 0511 { 0512 QString label, value; 0513 getInfoForKey(key, &label, &value); 0514 return value; 0515 } 0516 0517 QString ImageMetaInfoModel::keyForIndex(const QModelIndex &index) const 0518 { 0519 if (index.internalId() == NoGroup) { 0520 return {}; 0521 } 0522 MetaInfoGroup *group = d->mMetaInfoGroupVector[index.internalId()]; 0523 return group->getKeyAt(index.row()); 0524 } 0525 0526 QModelIndex ImageMetaInfoModel::index(int row, int col, const QModelIndex &parent) const 0527 { 0528 if (col < 0 || col > 1) { 0529 return {}; 0530 } 0531 if (!parent.isValid()) { 0532 // This is a group 0533 if (row < 0 || row >= d->mMetaInfoGroupVector.size()) { 0534 return {}; 0535 } 0536 return createIndex(row, col, col == 0 ? NoGroup : NoGroupSpace); 0537 } else { 0538 // This is an entry 0539 int group = parent.row(); 0540 if (row < 0 || row >= d->mMetaInfoGroupVector[group]->size()) { 0541 return {}; 0542 } 0543 return createIndex(row, col, group); 0544 } 0545 } 0546 0547 QModelIndex ImageMetaInfoModel::parent(const QModelIndex &index) const 0548 { 0549 if (!index.isValid()) { 0550 return {}; 0551 } 0552 if (index.internalId() == NoGroup || index.internalId() == NoGroupSpace) { 0553 return {}; 0554 } else { 0555 return createIndex(index.internalId(), 0, NoGroup); 0556 } 0557 } 0558 0559 int ImageMetaInfoModel::rowCount(const QModelIndex &parent) const 0560 { 0561 if (!parent.isValid()) { 0562 return d->mMetaInfoGroupVector.size(); 0563 } else if (parent.internalId() == NoGroup) { 0564 return d->mMetaInfoGroupVector[parent.row()]->size(); 0565 } else { 0566 return 0; 0567 } 0568 } 0569 0570 int ImageMetaInfoModel::columnCount(const QModelIndex & /*parent*/) const 0571 { 0572 return 2; 0573 } 0574 0575 QVariant ImageMetaInfoModel::data(const QModelIndex &index, int role) const 0576 { 0577 if (!index.isValid()) { 0578 return {}; 0579 } 0580 0581 switch (role) { 0582 case Qt::DisplayRole: 0583 return d->displayData(index); 0584 default: 0585 return {}; 0586 } 0587 } 0588 0589 QVariant ImageMetaInfoModel::headerData(int section, Qt::Orientation orientation, int role) const 0590 { 0591 if (orientation == Qt::Vertical || role != Qt::DisplayRole) { 0592 return {}; 0593 } 0594 0595 QString caption; 0596 if (section == 0) { 0597 caption = i18nc("@title:column", "Property"); 0598 } else if (section == 1) { 0599 caption = i18nc("@title:column", "Value"); 0600 } else { 0601 qCWarning(GWENVIEW_LIB_LOG) << "Unknown section" << section; 0602 } 0603 0604 return QVariant(caption); 0605 } 0606 0607 } // namespace 0608 0609 #include "moc_imagemetainfomodel.cpp"