File indexing completed on 2024-04-28 15:39:06
0001 // SPDX-FileCopyrightText: 2020-2022 Tobias Leupold <tl at stonemx dot de> 0002 // 0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 0005 // Local includes 0006 #include "ImagesModel.h" 0007 #include "KGeoTag.h" 0008 #include "Coordinates.h" 0009 0010 // KDE includes 0011 #include <KLocalizedString> 0012 #include <KExiv2/KExiv2> 0013 0014 // Qt includes 0015 #include <QFileInfo> 0016 #include <QFont> 0017 0018 // C++ includes 0019 #include <utility> 0020 0021 ImagesModel::ImagesModel(QObject *parent, bool splitImagesList, int thumbnailSize, int previewSize) 0022 : QAbstractListModel(parent), 0023 m_splitImagesList(splitImagesList), 0024 m_thumbnailSize(QSize(thumbnailSize, thumbnailSize)), 0025 m_previewSize(QSize(previewSize, previewSize)) 0026 { 0027 m_timeZone = QTimeZone::systemTimeZone(); 0028 } 0029 0030 void ImagesModel::setSplitImagesList(bool state) 0031 { 0032 m_splitImagesList = state; 0033 Q_EMIT dataChanged(index(0, 0), index(rowCount(), 0), { Qt::DisplayRole }); 0034 } 0035 0036 int ImagesModel::rowCount(const QModelIndex &) const 0037 { 0038 return m_paths.count(); 0039 } 0040 0041 QVariant ImagesModel::data(const QModelIndex &index, int role) const 0042 { 0043 if (! index.isValid() || index.row() > m_paths.count()) { 0044 return QVariant(); 0045 } 0046 0047 const auto &path = m_paths.at(index.row()); 0048 const auto &data = m_imageData[path]; 0049 0050 if (role == Qt::DisplayRole) { 0051 const QString associatedMarker = (! m_splitImagesList && data.coordinates.isSet()) 0052 ? i18nc("Marker for an associated file", "\u2713\u2009") 0053 : QString(); 0054 const QString changedmarker = data.coordinates != data.lastSavedCoordinates 0055 ? i18nc("Marker for a file with a pending change", "\u2009*") 0056 : QString(); 0057 return i18nc("Pattern for a display filename with a \"pending change\" and an " 0058 "\"associated\" marker. The first option is the \"associated\" marker, the " 0059 "second one is the filename and the third one the \"pending change\" marker.", 0060 "%1%2%3", 0061 associatedMarker, data.fileName, changedmarker); 0062 0063 } else if (role == Qt::DecorationRole) { 0064 return data.thumbnail; 0065 0066 } else if (role == Qt::ForegroundRole) { 0067 switch (data.matchType) { 0068 case KGeoTag::NotMatched: 0069 return m_colorScheme.foreground(); 0070 case KGeoTag::ExactMatch: 0071 return m_colorScheme.foreground(KColorScheme::PositiveText); 0072 case KGeoTag::InterpolatedMatch: 0073 return m_colorScheme.foreground(KColorScheme::NeutralText); 0074 case KGeoTag::ManuallySet: 0075 return m_colorScheme.foreground(KColorScheme::LinkText); 0076 } 0077 0078 } else if (! m_splitImagesList && role == Qt::FontRole) { 0079 QFont font; 0080 if (data.coordinates.isSet()) { 0081 font.setBold(true); 0082 } 0083 return font; 0084 0085 } else if (role == KGeoTag::PathRole) { 0086 return path; 0087 0088 } else if (role == KGeoTag::DateRole) { 0089 return data.date; 0090 0091 } else if (role == KGeoTag::CoordinatesRole) { 0092 QVariant coordinates; 0093 coordinates.setValue(data.coordinates); 0094 return coordinates; 0095 0096 } else if (role == KGeoTag::ThumbnailRole) { 0097 return data.thumbnail; 0098 0099 } else if (role == KGeoTag::PreviewRole) { 0100 return data.preview; 0101 0102 } else if (role == KGeoTag::MatchTypeRole) { 0103 QVariant matchType; 0104 matchType.setValue(data.matchType); 0105 return matchType; 0106 0107 } else if (role == KGeoTag::ChangedRole) { 0108 return data.originalCoordinates != data.coordinates; 0109 0110 } 0111 0112 return QVariant(); 0113 } 0114 0115 ImagesModel::LoadResult ImagesModel::addImage(const QString &path) 0116 { 0117 // Check if we already have the image 0118 if (m_paths.contains(path)) { 0119 return LoadResult::AlreadyLoaded; 0120 } 0121 0122 // Read the image 0123 QImage image = QImage(path); 0124 if (image.isNull()) { 0125 return LoadResult::LoadingImageFailed; 0126 } 0127 0128 // Read the exif data 0129 auto exif = KExiv2Iface::KExiv2(); 0130 exif.setUseXMPSidecar4Reading(true); 0131 if (! exif.load(path)) { 0132 return LoadResult::LoadingMetadataFailed; 0133 } 0134 0135 // Prepare the images's data struct 0136 ImageData data; 0137 0138 // Add the filename 0139 const QFileInfo info(path); 0140 data.fileName = info.fileName(); 0141 0142 // Read the date 0143 data.date = exif.getImageDateTime(); 0144 0145 // If no date could be read from the metadata, fall back to file properties 0146 if (! data.date.isValid()) { 0147 // First try to get the file's initial creation date 0148 data.date = info.birthTime(); 0149 0150 // If that fails, fall back to the file's mtime 0151 if (! data.date.isValid()) { 0152 data.date = info.lastModified(); 0153 } 0154 } 0155 0156 // Apply the currently set timezone 0157 data.date.setTimeZone(m_timeZone); 0158 0159 // Strip out milliseconds if the image provides them to allow seconds-exact matching 0160 const auto msec = data.date.time().msec(); 0161 if (msec != 0) { 0162 data.date = data.date.addMSecs(msec * -1); 0163 } 0164 0165 // Try to read gps information 0166 double altitude; 0167 double latitude; 0168 double longitude; 0169 if (exif.getGPSInfo(altitude, latitude, longitude)) { 0170 data.originalCoordinates = Coordinates(longitude, latitude, altitude, true); 0171 data.lastSavedCoordinates = data.originalCoordinates; 0172 data.coordinates = data.originalCoordinates; 0173 } 0174 0175 // Fix the image's orientation 0176 exif.rotateExifQImage(image, exif.getImageOrientation()); 0177 0178 // Create a smaller thumbnail 0179 data.thumbnail = QPixmap::fromImage(image.scaled(m_thumbnailSize, Qt::KeepAspectRatio, 0180 Qt::SmoothTransformation)); 0181 0182 // Create a bigger preview (to be scaled according to the view size) 0183 data.preview = image.scaled(m_previewSize, Qt::KeepAspectRatio); 0184 0185 // Find the correct row for the new image (sorted by date) 0186 int row = 0; 0187 for (const QString &path : m_paths) { 0188 if (m_imageData.value(path).date > data.date) { 0189 break; 0190 } 0191 row++; 0192 } 0193 0194 // Add the image 0195 0196 beginInsertRows(QModelIndex(), row, row); 0197 m_paths.insert(row, path); 0198 m_imageData.insert(path, data); 0199 endInsertRows(); 0200 0201 const auto modelIndex = index(row, 0); 0202 Q_EMIT dataChanged(modelIndex, modelIndex, { Qt::DisplayRole }); 0203 0204 return LoadResult::LoadingSucceeded; 0205 } 0206 0207 void ImagesModel::emitDataChanged(const QString &path) 0208 { 0209 const auto modelIndex = indexFor(path); 0210 Q_EMIT dataChanged(modelIndex, modelIndex, { Qt::DisplayRole }); 0211 } 0212 0213 const QVector<QString> &ImagesModel::allImages() const 0214 { 0215 return m_paths; 0216 } 0217 0218 QVector<QString> ImagesModel::imagesWithPendingChanges() const 0219 { 0220 QVector<QString> images; 0221 for (const auto &path : std::as_const(m_paths)) { 0222 const auto &data = m_imageData[path]; 0223 if (data.coordinates != data.lastSavedCoordinates) { 0224 images.append(path); 0225 } 0226 } 0227 return images; 0228 } 0229 0230 QVector<QString> ImagesModel::processedSavedImages() const 0231 { 0232 QVector<QString> images; 0233 for (const auto &path : std::as_const(m_paths)) { 0234 const auto &data = m_imageData[path]; 0235 if (data.changed && data.coordinates == data.lastSavedCoordinates) { 0236 images.append(path); 0237 } 0238 } 0239 return images; 0240 } 0241 0242 QVector<QString> ImagesModel::imagesLoadedTagged() const 0243 { 0244 QVector<QString> images; 0245 for (const auto &path : std::as_const(m_paths)) { 0246 const auto &data = m_imageData[path]; 0247 if (data.originalCoordinates.isSet()) { 0248 images.append(path); 0249 } 0250 } 0251 return images; 0252 } 0253 0254 QDateTime ImagesModel::date(const QString &path) const 0255 { 0256 return m_imageData.value(path).date; 0257 } 0258 0259 bool ImagesModel::contains(const QString &path) const 0260 { 0261 return m_paths.contains(path); 0262 } 0263 0264 Coordinates ImagesModel::coordinates(const QString &path) const 0265 { 0266 return m_imageData.value(path).coordinates; 0267 } 0268 0269 void ImagesModel::setCoordinates(const QString &path, const Coordinates &coordinates, 0270 KGeoTag::MatchType matchType) 0271 { 0272 auto &data = m_imageData[path]; 0273 data.matchType = matchType; 0274 data.coordinates = coordinates; 0275 data.changed = true; 0276 emitDataChanged(path); 0277 } 0278 0279 void ImagesModel::setElevation(const QString &path, double elevation) 0280 { 0281 auto &data = m_imageData[path]; 0282 data.coordinates.setAlt(elevation); 0283 data.changed = true; 0284 } 0285 0286 void ImagesModel::resetChanges(const QString &path) 0287 { 0288 auto &data = m_imageData[path]; 0289 data.coordinates = data.originalCoordinates; 0290 data.matchType = KGeoTag::NotMatched; 0291 emitDataChanged(path); 0292 } 0293 0294 QModelIndex ImagesModel::indexFor(const QString &path) const 0295 { 0296 return index(m_paths.indexOf(path), 0); 0297 } 0298 0299 void ImagesModel::setSaved(const QString &path) 0300 { 0301 auto &data = m_imageData[path]; 0302 data.lastSavedCoordinates = data.coordinates; 0303 } 0304 0305 KGeoTag::MatchType ImagesModel::matchType(const QString &path) const 0306 { 0307 return m_imageData.value(path).matchType; 0308 } 0309 0310 void ImagesModel::setImagesTimeZone(const QByteArray &id) 0311 { 0312 m_timeZone = QTimeZone(id); 0313 0314 for (const auto &path : m_paths) { 0315 m_imageData[path].date.setTimeZone(m_timeZone); 0316 } 0317 } 0318 0319 bool ImagesModel::hasPendingChanges(const QString &path) const 0320 { 0321 const auto &data = m_imageData[path]; 0322 return data.coordinates != data.lastSavedCoordinates; 0323 } 0324 0325 void ImagesModel::removeImages(const QVector<QString> &paths) 0326 { 0327 for (const auto &path : paths) { 0328 const auto row = m_paths.indexOf(path); 0329 const auto modelIndex = index(row, 0); 0330 beginRemoveRows(QModelIndex(), row, row); 0331 m_paths.remove(row); 0332 m_imageData.remove(path); 0333 Q_EMIT dataChanged(modelIndex, modelIndex, { Qt::DisplayRole }); 0334 endRemoveRows(); 0335 } 0336 } 0337 0338 void ImagesModel::removeAllImages() 0339 { 0340 const auto lastRow = m_paths.count() - 1; 0341 const auto firstModelIndex = index(0, 0); 0342 const auto lastModelIndex = index(lastRow, 0); 0343 beginRemoveRows(QModelIndex(), 0, lastRow); 0344 m_paths.clear(); 0345 m_imageData.clear(); 0346 Q_EMIT dataChanged(firstModelIndex, lastModelIndex, { Qt::DisplayRole }); 0347 endRemoveRows(); 0348 }