File indexing completed on 2024-04-28 15:40:25
0001 // SPDX-FileCopyrightText: 2003 - 2020 The KPhotoAlbum Development Team 0002 // SPDX-FileCopyrightText: 2021 - 2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at> 0003 // 0004 // SPDX-License-Identifier: GPL-2.0-or-later 0005 0006 #include "DescriptionUtil.h" 0007 0008 #include <DB/CategoryCollection.h> 0009 #include <DB/ImageDB.h> 0010 #include <kpabase/Logging.h> 0011 #include <kpabase/SettingsData.h> 0012 #include <kpaexif/Info.h> 0013 0014 #include <KLocalizedString> 0015 #include <QList> 0016 #include <QTextCodec> 0017 #include <QUrl> 0018 0019 namespace 0020 { 0021 const QLatin1String LINE_BREAK("<br/>"); 0022 } 0023 0024 /** 0025 * Add a line label + info text to the result text if info is not empty. 0026 * If the result already contains something, a HTML newline is added first. 0027 * To be used in createInfoText(). 0028 */ 0029 static void AddNonEmptyInfo(const QString &label, const QString &infoText, QString *result) 0030 { 0031 if (infoText.isEmpty()) { 0032 return; 0033 } 0034 if (!result->isEmpty()) { 0035 *result += LINE_BREAK; 0036 } 0037 result->append(label).append(infoText); 0038 } 0039 0040 /** 0041 * Given an ImageInfoPtr this function will create an HTML blob about the 0042 * image. The blob is used in the viewer and in the tool tip box from the 0043 * thumbnail view. 0044 * 0045 * As the HTML text is created, the parameter linkMap is filled with 0046 * information about hyperlinks. The map maps from an index to a pair of 0047 * (categoryName, categoryItem). This linkMap is used when the user selects 0048 * one of the hyberlinks. 0049 */ 0050 QString Utilities::createInfoText(DB::ImageInfoPtr info, QMap<int, QPair<QString, QString>> *linkMap) 0051 { 0052 Q_ASSERT(info); 0053 0054 QString result; 0055 if (Settings::SettingsData::instance()->showFilename()) { 0056 AddNonEmptyInfo(i18n("<b>File Name: </b> "), info->fileName().relative(), &result); 0057 } 0058 0059 if (Settings::SettingsData::instance()->showDate()) { 0060 QString dateString = info->date().toString(Settings::SettingsData::instance()->showTime() ? true : false); 0061 dateString.append(timeAgo(info)); 0062 AddNonEmptyInfo(i18n("<b>Date: </b> "), dateString, &result); 0063 } 0064 0065 /* XXX */ 0066 if (Settings::SettingsData::instance()->showImageSize() && info->mediaType() == DB::Image) { 0067 const QSize imageSize = info->size(); 0068 // Do not add -1 x -1 text 0069 if (imageSize.width() >= 0 && imageSize.height() >= 0) { 0070 const double megapix = imageSize.width() * imageSize.height() / 1000000.0; 0071 QString infoText = i18nc("width x height", "%1x%2", QString::number(imageSize.width()), QString::number(imageSize.height())); 0072 if (megapix > 0.05) { 0073 infoText += i18nc("short for: x megapixels", " (%1MP)", QString::number(megapix, 'f', 1)); 0074 } 0075 const double aspect = (double)imageSize.width() / (double)imageSize.height(); 0076 // 0.995 - 1.005 can still be considered quadratic 0077 if (aspect > 1.005) 0078 infoText += i18nc("aspect ratio", " (%1:1)", QLocale::system().toString(aspect, 'f', 2)); 0079 else if (aspect >= 0.995) 0080 infoText += i18nc("aspect ratio", " (1:1)"); 0081 else 0082 infoText += i18nc("aspect ratio", " (1:%1)", QLocale::system().toString(1.0 / aspect, 'f', 2)); 0083 AddNonEmptyInfo(i18n("<b>Image Size: </b> "), infoText, &result); 0084 } 0085 } 0086 0087 if (Settings::SettingsData::instance()->showRating()) { 0088 if (info->rating() != -1) { 0089 if (!result.isEmpty()) 0090 result += QString::fromLatin1("<br/>"); 0091 QUrl rating; 0092 rating.setScheme(QString::fromLatin1("kratingwidget")); 0093 // we don't use the host part, but if we don't set it, we can't use port: 0094 rating.setHost(QString::fromLatin1("int")); 0095 rating.setPort(qMin(qMax(static_cast<short int>(0), info->rating()), static_cast<short int>(10))); 0096 result += QString::fromLatin1("<img src=\"%1\"/>").arg(rating.toString(QUrl::None)); 0097 } 0098 } 0099 0100 const QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); 0101 int link = 0; 0102 for (const DB::CategoryPtr &category : categories) { 0103 const QString categoryName = category->name(); 0104 if (category->doShow()) { 0105 StringSet items = info->itemsOfCategory(categoryName); 0106 0107 if (DB::ImageDB::instance()->untaggedCategoryFeatureConfigured() 0108 && !Settings::SettingsData::instance()->untaggedImagesTagVisible()) { 0109 0110 if (categoryName == Settings::SettingsData::instance()->untaggedCategory()) { 0111 if (items.contains(Settings::SettingsData::instance()->untaggedTag())) { 0112 items.remove(Settings::SettingsData::instance()->untaggedTag()); 0113 } 0114 } 0115 } 0116 0117 if (!items.empty()) { 0118 QString title = QString::fromUtf8("<b>%1: </b> ").arg(category->name()); 0119 QString infoText; 0120 bool first = true; 0121 for (const QString &item : qAsConst(items)) { 0122 if (first) 0123 first = false; 0124 else 0125 infoText += QString::fromLatin1(", "); 0126 0127 if (linkMap) { 0128 ++link; 0129 (*linkMap)[link] = QPair<QString, QString>(categoryName, item); 0130 infoText += QString::fromLatin1("<a href=\"%1\">%2</a>").arg(link).arg(item); 0131 infoText += formatAge(category, item, info); 0132 } else 0133 infoText += item; 0134 } 0135 AddNonEmptyInfo(title, infoText, &result); 0136 } 0137 } 0138 } 0139 0140 if (Settings::SettingsData::instance()->showLabel()) { 0141 AddNonEmptyInfo(i18n("<b>Label: </b> "), info->label(), &result); 0142 } 0143 0144 if (Settings::SettingsData::instance()->showDescription() && !info->description().trimmed().isEmpty()) { 0145 AddNonEmptyInfo(i18n("<b>Description: </b> "), info->description(), 0146 &result); 0147 } 0148 0149 QString exifText; 0150 if (Settings::SettingsData::instance()->showEXIF()) { 0151 typedef QMap<QString, QStringList> ExifMap; 0152 typedef ExifMap::const_iterator ExifMapIterator; 0153 ExifMap exifMap = Exif::Info::instance()->infoForViewer(info->fileName(), Settings::SettingsData::instance()->iptcCharset()); 0154 0155 for (ExifMapIterator exifIt = exifMap.constBegin(); exifIt != exifMap.constEnd(); ++exifIt) { 0156 if (exifIt.key().startsWith(QString::fromLatin1("Exif."))) 0157 for (QStringList::const_iterator valuesIt = exifIt.value().constBegin(); valuesIt != exifIt.value().constEnd(); ++valuesIt) { 0158 QString exifName = exifIt.key().split(QChar::fromLatin1('.')).last(); 0159 AddNonEmptyInfo(QString::fromLatin1("<b>%1: </b> ").arg(exifName), 0160 *valuesIt, &exifText); 0161 } 0162 } 0163 0164 QString iptcText; 0165 for (ExifMapIterator exifIt = exifMap.constBegin(); exifIt != exifMap.constEnd(); ++exifIt) { 0166 if (!exifIt.key().startsWith(QString::fromLatin1("Exif."))) 0167 for (QStringList::const_iterator valuesIt = exifIt.value().constBegin(); valuesIt != exifIt.value().constEnd(); ++valuesIt) { 0168 QString iptcName = exifIt.key().split(QChar::fromLatin1('.')).last(); 0169 AddNonEmptyInfo(QString::fromLatin1("<b>%1: </b> ").arg(iptcName), 0170 *valuesIt, &iptcText); 0171 } 0172 } 0173 0174 if (!iptcText.isEmpty()) { 0175 if (exifText.isEmpty()) 0176 exifText = iptcText; 0177 else 0178 exifText += QString::fromLatin1("<hr>") + iptcText; 0179 } 0180 } 0181 0182 if (!result.isEmpty() && !exifText.isEmpty()) 0183 result += QString::fromLatin1("<hr>"); 0184 result += exifText; 0185 0186 return result; 0187 } 0188 0189 namespace 0190 { 0191 enum class TimeUnit { 0192 /** Denotes a negative age. */ 0193 Invalid, 0194 Days, 0195 Months, 0196 Years 0197 }; 0198 class AgeSpec 0199 { 0200 public: 0201 /** 0202 * @brief The I18nContext enum determines how an age is displayed. 0203 */ 0204 enum class I18nContext { 0205 /// For birthdays, e.g. "Jesper was 30 years in this image". 0206 Birthday, 0207 /// For ages of events, e.g. "This image was taken 30 years ago". 0208 Anniversary 0209 }; 0210 int age; ///< The number of \c units, e.g. the "5" in "5 days" 0211 TimeUnit unit; 0212 0213 AgeSpec(); 0214 AgeSpec(int age, TimeUnit unit); 0215 0216 /** 0217 * @brief format 0218 * @param context the context where the formatted age is used. 0219 * @return a localized string describing the time range. 0220 */ 0221 QString format(I18nContext context) const; 0222 /** 0223 * @brief isValid 0224 * @return \c true, if the AgeSpec contains a valid age that is not negative. \c false otherwise. 0225 */ 0226 bool isValid() const; 0227 bool operator==(const AgeSpec &other) const; 0228 }; 0229 0230 AgeSpec::AgeSpec() 0231 : age(70) 0232 , unit(TimeUnit::Invalid) 0233 { 0234 } 0235 0236 AgeSpec::AgeSpec(int age, TimeUnit unit) 0237 : age(age) 0238 , unit(unit) 0239 { 0240 } 0241 0242 QString AgeSpec::format(I18nContext context) const 0243 { 0244 switch (unit) { 0245 case TimeUnit::Invalid: 0246 return {}; 0247 case TimeUnit::Days: 0248 if (context == I18nContext::Birthday) 0249 return i18ncp("As in 'The baby is 1 day old'", "1 day", "%1 days", age); 0250 else 0251 return i18ncp("As in 'This happened 1 day ago'", "1 day ago", "%1 days ago", age); 0252 case TimeUnit::Months: 0253 if (context == I18nContext::Birthday) 0254 return i18ncp("As in 'The baby is 1 month old'", "1 month", "%1 months", age); 0255 else 0256 return i18ncp("As in 'This happened 1 month ago'", "1 month ago", "%1 months ago", age); 0257 case TimeUnit::Years: 0258 if (context == I18nContext::Birthday) 0259 return i18ncp("As in 'The baby is 1 year old'", "1 year", "%1 years", age); 0260 else 0261 return i18ncp("As in 'This happened 1 year ago'", "1 year ago", "%1 years ago", age); 0262 } 0263 Q_ASSERT(false); 0264 return {}; 0265 } 0266 0267 bool AgeSpec::isValid() const 0268 { 0269 return unit != TimeUnit::Invalid; 0270 } 0271 0272 bool AgeSpec::operator==(const AgeSpec &other) const 0273 { 0274 return (age == other.age && unit == other.unit); 0275 } 0276 0277 /** 0278 * @brief dateDifference computes the difference between two dates with an appropriate unit. 0279 * It can be used to generate human readable date differences, 0280 * e.g. "6 months" instead of "0.5 years". 0281 * 0282 * @param priorDate 0283 * @param laterDate 0284 * @return a DateSpec with appropriate scale. 0285 */ 0286 AgeSpec dateDifference(const QDate &priorDate, const QDate &laterDate) 0287 { 0288 const int priorDay = priorDate.day(); 0289 const int laterDay = laterDate.day(); 0290 const int priorMonth = priorDate.month(); 0291 const int laterMonth = laterDate.month(); 0292 const int priorYear = priorDate.year(); 0293 const int laterYear = laterDate.year(); 0294 0295 // Image before birth 0296 const int days = priorDate.daysTo(laterDate); 0297 if (days < 0) 0298 return {}; 0299 0300 if (days < 31) 0301 return { days, TimeUnit::Days }; 0302 0303 int months = (laterYear - priorYear) * 12; 0304 months += (laterMonth - priorMonth); 0305 months += (laterDay >= priorDay) ? 0 : -1; 0306 0307 if (months < 24) 0308 return { months, TimeUnit::Months }; 0309 else 0310 return { months / 12, TimeUnit::Years }; 0311 } 0312 0313 #ifdef TEST_DATEDIFF 0314 0315 bool compare(const QString &actual, const QString &expected) 0316 { 0317 const bool isEqual = (actual == expected); 0318 if (!isEqual) { 0319 qDebug() << "Expected:" << expected << ", got:" << actual; 0320 } 0321 return isEqual; 0322 } 0323 void testDateDifference() 0324 { 0325 using namespace Utilities; 0326 bool passed = true; 0327 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1971, 7, 11)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("0 days")); 0328 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1971, 8, 10)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("30 days")); 0329 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1971, 8, 11)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("1 month")); 0330 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1971, 8, 12)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("1 month")); 0331 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1971, 9, 10)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("1 month")); 0332 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1971, 9, 11)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("2 month")); 0333 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1972, 6, 10)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("10 month")); 0334 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1972, 6, 11)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("11 month")); 0335 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1972, 6, 12)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("11 month")); 0336 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1972, 7, 10)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("11 month")); 0337 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1972, 7, 11)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("12 month")); 0338 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1972, 7, 12)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("12 month")); 0339 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1972, 12, 11)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("17 month")); 0340 passed &= compare(dateDifference(QDate(1971, 7, 11), QDate(1973, 7, 11)).format(AgeSpec::I18nContext::Birthday), QString::fromLatin1("2 years")); 0341 Q_ASSERT(passed); 0342 qDebug() << "Tested dateDifference without problems."; 0343 } 0344 #endif 0345 } 0346 0347 QString Utilities::formatAge(DB::CategoryPtr category, const QString &item, DB::ImageInfoPtr info) 0348 { 0349 #ifdef TEST_DATEDIFF 0350 testDateDifference(); // I wish I could get my act together to set up a test suite. 0351 #endif 0352 const QDate birthDate = category->birthDate(item); 0353 const QDate start = info->date().start().date(); 0354 const QDate end = info->date().end().date(); 0355 0356 if (birthDate.isNull() || !info->date().isValid()) 0357 return {}; 0358 0359 const AgeSpec minAge = dateDifference(birthDate, start); 0360 const AgeSpec maxAge = dateDifference(birthDate, end); 0361 0362 if (!minAge.isValid() && !maxAge.isValid()) 0363 return {}; // This is for example a person on an image before their birth 0364 if (minAge == maxAge) 0365 return i18n(" (%1)", minAge.format(AgeSpec::I18nContext::Birthday)); 0366 else if (!minAge.isValid()) 0367 return i18n(" (< %1)", maxAge.format(AgeSpec::I18nContext::Birthday)); 0368 else { 0369 if (minAge.unit == maxAge.unit) 0370 return i18nc("E.g. ' (1-2 years)'", " (%1-%2)", minAge.age, maxAge.format(AgeSpec::I18nContext::Birthday)); 0371 else 0372 return i18nc("E.g. ' (7 months-1 year)'", " (%1-%2)", minAge.format(AgeSpec::I18nContext::Birthday), maxAge.format(AgeSpec::I18nContext::Birthday)); 0373 } 0374 } 0375 0376 QString Utilities::timeAgo(const DB::ImageInfoPtr info) 0377 { 0378 const QDate startDate = info->date().start().date(); 0379 const QDate endDate = info->date().end().date(); 0380 const QDate today = QDate::currentDate(); 0381 0382 if (!info->date().isValid()) 0383 return {}; 0384 0385 const AgeSpec minTimeAgo = dateDifference(endDate, today); 0386 const AgeSpec maxTimeAgo = dateDifference(startDate, today); 0387 if (!minTimeAgo.isValid()) { 0388 return {}; 0389 } 0390 if (minTimeAgo == maxTimeAgo) { 0391 return i18n(" (%1)", minTimeAgo.format(AgeSpec::I18nContext::Anniversary)); 0392 } else { 0393 if (minTimeAgo.unit == maxTimeAgo.unit) 0394 return i18nc("E.g. ' (1-2 years ago)'", " (%1-%2)", minTimeAgo.age, maxTimeAgo.format(AgeSpec::I18nContext::Anniversary)); 0395 else 0396 return i18nc("E.g. '(7 months ago-1 year ago)'", " (%1-%2)", minTimeAgo.format(AgeSpec::I18nContext::Anniversary), maxTimeAgo.format(AgeSpec::I18nContext::Anniversary)); 0397 } 0398 }