File indexing completed on 2024-04-28 04:18:51

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"