using namespace Exif;
namespace
{
// schema version; bump it up whenever the database schema changes
constexpr int DB_VERSION = 3;
const Database::ElementList elements(int since = 0)
{
static Database::ElementList elms;
static int sinceDBVersion[DB_VERSION] {};
if (elms.count() == 0) {
elms.append(new RationalExifElement("Exif.Photo.FocalLength"));
elms.append(new RationalExifElement("Exif.Photo.ExposureTime"));
elms.append(new RationalExifElement("Exif.Photo.ApertureValue"));
elms.append(new RationalExifElement("Exif.Photo.FNumber"));
//elms.append( new RationalExifElement( "Exif.Photo.FlashEnergy" ) );
elms.append(new IntExifElement("Exif.Photo.Flash"));
elms.append(new IntExifElement("Exif.Photo.Contrast"));
elms.append(new IntExifElement("Exif.Photo.Sharpness"));
elms.append(new IntExifElement("Exif.Photo.Saturation"));
elms.append(new IntExifElement("Exif.Image.Orientation"));
elms.append(new IntExifElement("Exif.Photo.MeteringMode"));
elms.append(new IntExifElement("Exif.Photo.ISOSpeedRatings"));
elms.append(new IntExifElement("Exif.Photo.ExposureProgram"));
elms.append(new StringExifElement("Exif.Image.Make"));
elms.append(new StringExifElement("Exif.Image.Model"));
// gps info has been added in database schema version 2:
sinceDBVersion[1] = elms.size();
elms.append(new IntExifElement("Exif.GPSInfo.GPSVersionID")); // actually a byte value
elms.append(new RationalExifElement("Exif.GPSInfo.GPSAltitude"));
elms.append(new IntExifElement("Exif.GPSInfo.GPSAltitudeRef")); // actually a byte value
elms.append(new StringExifElement("Exif.GPSInfo.GPSMeasureMode"));
elms.append(new RationalExifElement("Exif.GPSInfo.GPSDOP"));
elms.append(new RationalExifElement("Exif.GPSInfo.GPSImgDirection"));
elms.append(new RationalExifElement("Exif.GPSInfo.GPSLatitude"));
elms.append(new StringExifElement("Exif.GPSInfo.GPSLatitudeRef"));
elms.append(new RationalExifElement("Exif.GPSInfo.GPSLongitude"));
elms.append(new StringExifElement("Exif.GPSInfo.GPSLongitudeRef"));
elms.append(new RationalExifElement("Exif.GPSInfo.GPSTimeStamp"));
// lens info has been added in database schema version 3:
sinceDBVersion[2] = elms.size();
elms.append(new LensExifElement());
}
// query only for the newly added stuff:
if (since > 0)
return elms.mid(sinceDBVersion[since]);
return elms;
}
}
Exif::Database *Exif::Database::s_instance = nullptr;
/**
* @brief show and error message for the failed \p query and disable the Exif database.
* The database is closed because at this point we can not trust the data inside.
* @param query
*/
void Database::showErrorAndFail(QSqlQuery &query) const
{
const QString txt = i18n("There was an error while accessing the Exif search database. "
"The error is likely due to a broken database file.
"
"To fix this problem run Maintenance->Recreate Exif Search database.
"
"
"
"For debugging: the command that was attempted to be executed was:
%1
"
"The error message obtained was:
%2
",
query.lastQuery(), query.lastError().text());
const QString technicalInfo = QString::fromUtf8("Error running query: %s\n Error was: %s")
.arg(query.lastQuery(), query.lastError().text());
showErrorAndFail(txt, technicalInfo);
}
void Database::showErrorAndFail(const QString &errorMessage, const QString &technicalInfo) const
{
KMessageBox::information(MainWindow::Window::theMainWindow(), errorMessage, i18n("Error in Exif database"), QString::fromLatin1("sql_error_in_exif_DB"));
qCWarning(ExifLog) << technicalInfo;
// disable exif db for now:
m_isFailed = true;
}
Exif::Database::Database()
: m_isOpen(false)
, m_isFailed(false)
{
m_db = QSqlDatabase::addDatabase(QString::fromLatin1("QSQLITE"), QString::fromLatin1("exif"));
}
void Exif::Database::openDatabase()
{
m_db.setDatabaseName(exifDBFile());
m_isOpen = m_db.open();
if (!m_isOpen) {
const QString txt = i18n("There was an error while opening the Exif search database.
"
"To fix this problem run Maintenance->Recreate Exif Search database.
"
"
"
"The error message obtained was:
%1
",
m_db.lastError().text());
const QString logMsg = QString::fromUtf8("Could not open Exif search database! "
"Error was: %s")
.arg(m_db.lastError().text());
showErrorAndFail(txt, logMsg);
return;
}
// If SQLite in Qt has Unicode feature, it will convert queries to
// UTF-8 automatically. Otherwise we should do the conversion to
// be able to store any Unicode character.
m_doUTF8Conversion = !m_db.driver()->hasFeature(QSqlDriver::Unicode);
}
Exif::Database::~Database()
{
// We have to close the database before destroying the QSqlDatabase object,
// otherwise Qt screams and kittens might die (see QSqlDatabase's
// documentation)
if (m_db.isOpen())
m_db.close();
}
bool Exif::Database::isOpen() const
{
return m_isOpen && !m_isFailed;
}
void Exif::Database::populateDatabase()
{
createMetadataTable(SchemaAndDataChanged);
QStringList attributes;
- Q_FOREACH (DatabaseElement *element, elements()) {
+ for (DatabaseElement *element : elements()) {
attributes.append(element->createString());
}
QSqlQuery query(QString::fromLatin1("create table if not exists exif (filename string PRIMARY KEY, %1 )")
.arg(attributes.join(QString::fromLatin1(", "))),
m_db);
if (!query.exec())
showErrorAndFail(query);
}
void Exif::Database::updateDatabase()
{
if (m_db.tables().isEmpty()) {
const QString txt = i18n("The Exif search database is corrupted and has no data.
"
"To fix this problem run Maintenance->Recreate Exif Search database.
");
const QString logMsg = QString::fromUtf8("Database open but empty!");
showErrorAndFail(txt, logMsg);
return;
}
const int version = DBFileVersion();
if (m_isFailed)
return;
if (version < DBVersion()) {
// on the next update, we can just query the DB Version
createMetadataTable(SchemaChanged);
}
// update schema
if (version < DBVersion()) {
QSqlQuery query(m_db);
for (const DatabaseElement *e : elements(version)) {
query.prepare(QString::fromLatin1("alter table exif add column %1")
.arg(e->createString()));
if (!query.exec())
showErrorAndFail(query);
}
}
}
void Exif::Database::createMetadataTable(DBSchemaChangeType change)
{
QSqlQuery query(m_db);
query.prepare(QString::fromLatin1("create table if not exists settings (keyword TEXT PRIMARY KEY, value TEXT) without rowid"));
if (!query.exec()) {
showErrorAndFail(query);
return;
}
query.prepare(QString::fromLatin1("insert or replace into settings (keyword, value) values('DBVersion','%1')").arg(Database::DBVersion()));
if (!query.exec()) {
showErrorAndFail(query);
return;
}
if (change == SchemaAndDataChanged) {
query.prepare(QString::fromLatin1("insert or replace into settings (keyword, value) values('GuaranteedDataVersion','%1')").arg(Database::DBVersion()));
if (!query.exec())
showErrorAndFail(query);
}
}
bool Exif::Database::add(const DB::FileName &fileName)
{
if (!isUsable())
return false;
try {
Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fileName.absolute().toLocal8Bit().data());
Q_ASSERT(image.get() != nullptr);
image->readMetadata();
Exiv2::ExifData &exifData = image->exifData();
return insert(fileName, exifData);
} catch (...) {
qCWarning(ExifLog, "Error while reading exif information from %s", qPrintable(fileName.absolute()));
return false;
}
}
bool Exif::Database::add(DB::FileInfo &fileInfo)
{
if (!isUsable())
return false;
return insert(fileInfo.getFileName(), fileInfo.getExifData());
}
bool Exif::Database::add(const DB::FileNameList &list)
{
if (!isUsable())
return false;
QList map;
- Q_FOREACH (const DB::FileName &fileName, list) {
+ for (const DB::FileName &fileName : list) {
try {
Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fileName.absolute().toLocal8Bit().data());
Q_ASSERT(image.get() != nullptr);
image->readMetadata();
map << DBExifInfo(fileName, image->exifData());
} catch (...) {
qWarning("Error while reading exif information from %s", qPrintable(fileName.absolute()));
}
}
insert(map);
return true;
}
void Exif::Database::remove(const DB::FileName &fileName)
{
if (!isUsable())
return;
QSqlQuery query(m_db);
query.prepare(QString::fromLatin1("DELETE FROM exif WHERE fileName=?"));
query.bindValue(0, fileName.absolute());
if (!query.exec())
showErrorAndFail(query);
}
void Exif::Database::remove(const DB::FileNameList &list)
{
if (!isUsable())
return;
m_db.transaction();
QSqlQuery query(m_db);
query.prepare(QString::fromLatin1("DELETE FROM exif WHERE fileName=?"));
- Q_FOREACH (const DB::FileName &fileName, list) {
+ for (const DB::FileName &fileName : list) {
query.bindValue(0, fileName.absolute());
if (!query.exec()) {
m_db.rollback();
showErrorAndFail(query);
return;
}
}
m_db.commit();
}
QSqlQuery *Exif::Database::getInsertQuery()
{
if (!isUsable())
return nullptr;
if (m_insertTransaction)
return m_insertTransaction;
if (m_queryString.isEmpty()) {
QStringList formalList;
Database::ElementList elms = elements();
for (const DatabaseElement *e : elms) {
formalList.append(e->queryString());
}
m_queryString = QString::fromLatin1("INSERT OR REPLACE into exif values (?, %1) ").arg(formalList.join(QString::fromLatin1(", ")));
}
QSqlQuery *query = new QSqlQuery(m_db);
if (query)
query->prepare(m_queryString);
return query;
}
void Exif::Database::concludeInsertQuery(QSqlQuery *query)
{
if (m_insertTransaction)
return;
m_db.commit();
delete query;
}
bool Exif::Database::startInsertTransaction()
{
Q_ASSERT(m_insertTransaction == nullptr);
m_insertTransaction = getInsertQuery();
m_db.transaction();
return (m_insertTransaction != nullptr);
}
bool Exif::Database::commitInsertTransaction()
{
if (m_insertTransaction) {
m_db.commit();
delete m_insertTransaction;
m_insertTransaction = nullptr;
} else
qCWarning(ExifLog, "Trying to commit transaction, but no transaction is active!");
return true;
}
bool Exif::Database::abortInsertTransaction()
{
if (m_insertTransaction) {
m_db.rollback();
delete m_insertTransaction;
m_insertTransaction = nullptr;
} else
qCWarning(ExifLog, "Trying to abort transaction, but no transaction is active!");
return true;
}
bool Exif::Database::insert(const DB::FileName &filename, Exiv2::ExifData data)
{
if (!isUsable())
return false;
QSqlQuery *query = getInsertQuery();
query->bindValue(0, filename.absolute());
int i = 1;
for (const DatabaseElement *e : elements()) {
query->bindValue(i++, e->valueFromExif(data));
}
bool status = query->exec();
if (!status)
showErrorAndFail(*query);
concludeInsertQuery(query);
return status;
}
bool Exif::Database::insert(QList map)
{
if (!isUsable())
return false;
QSqlQuery *query = getInsertQuery();
// not a const reference because DatabaseElement::valueFromExif uses operator[] on the exif datum
- Q_FOREACH (DBExifInfo elt, map) {
+ for (DBExifInfo elt : map) {
query->bindValue(0, elt.first.absolute());
int i = 1;
for (const DatabaseElement *e : elements()) {
query->bindValue(i++, e->valueFromExif(elt.second));
}
if (!query->exec()) {
showErrorAndFail(*query);
}
}
concludeInsertQuery(query);
return true;
}
Exif::Database *Exif::Database::instance()
{
if (!s_instance) {
qCInfo(ExifLog) << "initializing Exif database...";
s_instance = new Exif::Database();
s_instance->init();
}
return s_instance;
}
void Exif::Database::deleteInstance()
{
delete s_instance;
s_instance = nullptr;
}
bool Exif::Database::isAvailable()
{
#ifdef QT_NO_SQL
return false;
#else
return QSqlDatabase::isDriverAvailable(QString::fromLatin1("QSQLITE"));
#endif
}
int Exif::Database::DBFileVersion() const
{
// previous to KPA 4.6, there was no metadata table:
if (!m_db.tables().contains(QString::fromLatin1("settings")))
return 1;
QSqlQuery query(QString::fromLatin1("SELECT value FROM settings WHERE keyword = 'DBVersion'"), m_db);
if (!query.exec())
showErrorAndFail(query);
if (query.first()) {
return query.value(0).toInt();
}
return 0;
}
int Exif::Database::DBFileVersionGuaranteed() const
{
// previous to KPA 4.6, there was no metadata table:
if (!m_db.tables().contains(QString::fromLatin1("settings")))
return 0;
QSqlQuery query(QString::fromLatin1("SELECT value FROM settings WHERE keyword = 'GuaranteedDataVersion'"), m_db);
if (!query.exec())
showErrorAndFail(query);
if (query.first()) {
return query.value(0).toInt();
}
return 0;
}
constexpr int Exif::Database::DBVersion()
{
return DB_VERSION;
}
bool Exif::Database::isUsable() const
{
return (isAvailable() && isOpen());
}
QString Exif::Database::exifDBFile()
{
return ::Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1("/exif-info.db");
}
bool Exif::Database::readFields(const DB::FileName &fileName, ElementList &fields) const
{
if (!isUsable())
return false;
bool foundIt = false;
QStringList fieldList;
for (const DatabaseElement *e : fields) {
fieldList.append(e->columnName());
}
QSqlQuery query(m_db);
// the query returns a single value, so we don't need the overhead for random access:
query.setForwardOnly(true);
query.prepare(QString::fromLatin1("select %1 from exif where filename=?")
.arg(fieldList.join(QString::fromLatin1(", "))));
query.bindValue(0, fileName.absolute());
if (!query.exec()) {
showErrorAndFail(query);
}
if (query.next()) {
// file in exif db -> write back results
int i = 0;
for (DatabaseElement *e : fields) {
e->setValue(query.value(i++));
}
foundIt = true;
}
return foundIt;
}
DB::FileNameSet Exif::Database::filesMatchingQuery(const QString &queryStr) const
{
if (!isUsable())
return DB::FileNameSet();
DB::FileNameSet result;
QSqlQuery query(queryStr, m_db);
if (!query.exec())
showErrorAndFail(query);
else {
if (m_doUTF8Conversion)
while (query.next())
result.insert(DB::FileName::fromAbsolutePath(QString::fromUtf8(query.value(0).toByteArray())));
else
while (query.next())
result.insert(DB::FileName::fromAbsolutePath(query.value(0).toString()));
}
return result;
}
QList> Exif::Database::cameras() const
{
QList> result;
if (!isUsable())
return result;
QSqlQuery query(QString::fromLatin1("SELECT DISTINCT Exif_Image_Make, Exif_Image_Model FROM exif"), m_db);
if (!query.exec()) {
showErrorAndFail(query);
} else {
while (query.next()) {
QString make = query.value(0).toString();
QString model = query.value(1).toString();
if (!make.isEmpty() && !model.isEmpty())
result.append(qMakePair(make, model));
}
}
return result;
}
QList Exif::Database::lenses() const
{
QList result;
if (!isUsable())
return result;
QSqlQuery query(QString::fromLatin1("SELECT DISTINCT Exif_Photo_LensModel FROM exif"), m_db);
if (!query.exec()) {
showErrorAndFail(query);
} else {
while (query.next()) {
QString lens = query.value(0).toString();
if (!lens.isEmpty())
result.append(lens);
}
}
return result;
}
void Exif::Database::init()
{
if (!isAvailable())
return;
m_isFailed = false;
m_insertTransaction = nullptr;
bool dbExists = QFile::exists(exifDBFile());
openDatabase();
if (!isOpen())
return;
if (!dbExists)
populateDatabase();
else
updateDatabase();
}
void Exif::Database::recreate()
{
// We create a backup of the current database in case
// the user presse 'cancel' or there is any error. In that case
// we want to go back to the original DB.
const QString origBackup = exifDBFile() + QLatin1String(".bak");
m_db.close();
QDir().remove(origBackup);
QDir().rename(exifDBFile(), origBackup);
init();
const DB::FileNameList allImages = DB::ImageDB::instance()->images();
QProgressDialog dialog;
dialog.setModal(true);
dialog.setLabelText(i18n("Rereading Exif information from all images"));
dialog.setMaximum(allImages.size());
// using a transaction here removes a *huge* overhead on the insert statements
startInsertTransaction();
int i = 0;
for (const DB::FileName &fileName : allImages) {
const DB::ImageInfoPtr info = fileName.info();
dialog.setValue(i++);
if (info->mediaType() == DB::Image) {
add(fileName);
}
if (i % 10)
qApp->processEvents();
if (dialog.wasCanceled())
break;
}
// PENDING(blackie) We should count the amount of files that did not succeeded and warn the user.
if (dialog.wasCanceled()) {
abortInsertTransaction();
m_db.close();
QDir().remove(exifDBFile());
QDir().rename(origBackup, exifDBFile());
init();
} else {
commitInsertTransaction();
QDir().remove(origBackup);
}
}
// vi:expandtab:tabstop=4 shiftwidth=4:
diff --git a/Exif/SearchInfo.cpp b/Exif/SearchInfo.cpp
index cc21bf75..fc8fc869 100644
--- a/Exif/SearchInfo.cpp
+++ b/Exif/SearchInfo.cpp
@@ -1,207 +1,208 @@
-/* Copyright (C) 2003-2019 The KPhotoAlbum Development Team
+/* Copyright (C) 2003-2020 The KPhotoAlbum Development Team
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.
+ 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) version 3 or any later version
+ accepted by the membership of KDE e. V. (or its successor approved
+ by the membership of KDE e. V.), which shall act as a proxy
+ defined in Section 14 of version 3 of the license.
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.
+ 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; see the file COPYING. If not, write to
- the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
- Boston, MA 02110-1301, USA.
+ along with this program. If not, see .
*/
#include "SearchInfo.h"
#include "Database.h"
#include
#include
/**
* \class Exif::SearchInfo
* This class represents a search for Exif information. It is similar in functionality for category searches which is in the
* class \ref DB::ImageSearchInfo.
*
* The search is build, from \ref Exif::SearchDialog, using the functions addRangeKey(), addSearchKey(), and addCamara().
* The search is stored in an instance of \ref DB::ImageSearchInfo, and may later be executed using search().
* Once a search has been executed, the application may ask if a given image is in the search result using matches()
*/
void Exif::SearchInfo::addSearchKey(const QString &key, const IntList &values)
{
m_intKeys.append(qMakePair(key, values));
}
QStringList Exif::SearchInfo::buildIntKeyQuery() const
{
QStringList andArgs;
for (IntKeyList::ConstIterator intIt = m_intKeys.begin(); intIt != m_intKeys.end(); ++intIt) {
QStringList orArgs;
QString key = (*intIt).first;
IntList values = (*intIt).second;
- Q_FOREACH (int value, values) {
+ for (int value : values) {
orArgs << QString::fromLatin1("(%1 == %2)").arg(key).arg(value);
}
if (orArgs.count() != 0)
andArgs << QString::fromLatin1("(%1)").arg(orArgs.join(QString::fromLatin1(" or ")));
}
return andArgs;
}
void Exif::SearchInfo::addRangeKey(const Range &range)
{
m_rangeKeys.append(range);
}
Exif::SearchInfo::Range::Range(const QString &key)
: isLowerMin(false)
, isLowerMax(false)
, isUpperMin(false)
, isUpperMax(false)
, key(key)
{
}
QString Exif::SearchInfo::buildQuery() const
{
QStringList subQueries;
subQueries += buildIntKeyQuery();
subQueries += buildRangeQuery();
QString cameraQuery = buildCameraSearchQuery();
if (!cameraQuery.isEmpty())
subQueries.append(cameraQuery);
QString lensQuery = buildLensSearchQuery();
if (!lensQuery.isEmpty())
subQueries.append(lensQuery);
if (subQueries.empty())
return QString();
else
return QString::fromLatin1("SELECT filename from exif WHERE %1")
.arg(subQueries.join(QString::fromLatin1(" and ")));
}
QStringList Exif::SearchInfo::buildRangeQuery() const
{
QStringList result;
for (QList::ConstIterator it = m_rangeKeys.begin(); it != m_rangeKeys.end(); ++it) {
QString str = sqlForOneRangeItem(*it);
if (!str.isEmpty())
result.append(str);
}
return result;
}
QString Exif::SearchInfo::sqlForOneRangeItem(const Range &range) const
{
// Notice I multiplied factors on each value to ensure that we do not fail due to rounding errors for say 1/3
if (range.isLowerMin) {
// Min to Min means < x
if (range.isUpperMin)
return QString::fromLatin1("%1 < %2 and %3 > 0").arg(range.key).arg(range.min * 1.01).arg(range.key);
// Min to Max means all images
if (range.isUpperMax)
return QString();
// Min to y means <= y
return QString::fromLatin1("%1 <= %2 and %3 > 0").arg(range.key).arg(range.max * 1.01).arg(range.key);
}
// MAX to MAX means >= y
if (range.isLowerMax)
return QString::fromLatin1("%1 > %2").arg(range.key).arg(range.max * 0.99);
// x to Max means >= x
if (range.isUpperMax)
return QString::fromLatin1("%1 >= %2").arg(range.key).arg(range.min * 0.99);
// x to y means >=x and <=y
return QString::fromLatin1("(%1 <= %2 and %3 <= %4)")
.arg(range.min * 0.99)
.arg(range.key)
.arg(range.key)
.arg(range.max * 1.01);
}
void Exif::SearchInfo::search() const
{
QString queryStr = buildQuery();
m_emptyQuery = queryStr.isEmpty();
// ensure to do SQL queries as little as possible.
static QString lastQuery;
if (queryStr == lastQuery)
return;
lastQuery = queryStr;
m_matches.clear();
if (m_emptyQuery)
return;
m_matches = Exif::Database::instance()->filesMatchingQuery(queryStr);
}
bool Exif::SearchInfo::matches(const DB::FileName &fileName) const
{
if (m_emptyQuery)
return true;
return m_matches.contains(fileName);
}
bool Exif::SearchInfo::isNull() const
{
return buildQuery().isEmpty();
}
void Exif::SearchInfo::addCamera(const CameraList &list)
{
m_cameras = list;
}
void Exif::SearchInfo::addLens(const LensList &list)
{
m_lenses = list;
}
QString Exif::SearchInfo::buildCameraSearchQuery() const
{
QStringList subResults;
for (CameraList::ConstIterator cameraIt = m_cameras.begin(); cameraIt != m_cameras.end(); ++cameraIt) {
subResults.append(QString::fromUtf8("(Exif_Image_Make='%1' and Exif_Image_Model='%2')")
.arg((*cameraIt).first)
.arg((*cameraIt).second));
}
if (subResults.count() != 0)
return QString::fromUtf8("(%1)").arg(subResults.join(QString::fromLatin1(" or ")));
else
return QString();
}
QString Exif::SearchInfo::buildLensSearchQuery() const
{
QStringList subResults;
for (LensList::ConstIterator lensIt = m_lenses.begin(); lensIt != m_lenses.end(); ++lensIt) {
if (*lensIt == i18nc("As in No persons, no locations etc.", "None"))
// compare to null (=entry from old db schema) and empty string (=entry w/o exif lens info)
subResults.append(QString::fromUtf8("(nullif(Exif_Photo_LensModel,'') is null)"));
else
subResults.append(QString::fromUtf8("(Exif_Photo_LensModel='%1')")
.arg(*lensIt));
}
if (subResults.count() != 0)
return QString::fromUtf8("(%1)").arg(subResults.join(QString::fromLatin1(" or ")));
else
return QString();
}
// vi:expandtab:tabstop=4 shiftwidth=4:
diff --git a/Exif/TreeView.cpp b/Exif/TreeView.cpp
index 90527098..0825e87a 100644
--- a/Exif/TreeView.cpp
+++ b/Exif/TreeView.cpp
@@ -1,104 +1,104 @@
-/* Copyright (C) 2003-2019 The KPhotoAlbum Development Team
+/* Copyright (C) 2003-2020 The KPhotoAlbum Development Team
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.
+ 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) version 3 or any later version
+ accepted by the membership of KDE e. V. (or its successor approved
+ by the membership of KDE e. V.), which shall act as a proxy
+ defined in Section 14 of version 3 of the license.
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.
+ 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; see the file COPYING. If not, write to
- the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
- Boston, MA 02110-1301, USA.
+ along with this program. If not, see .
*/
#include "TreeView.h"
-
#include "Info.h"
#include
-#include
-#include
+#include
+#include
using Utilities::StringSet;
Exif::TreeView::TreeView(const QString &title, QWidget *parent)
: QTreeWidget(parent)
{
setHeaderLabel(title);
reload();
connect(this, &TreeView::itemClicked, this, &TreeView::toggleChildren);
}
void Exif::TreeView::toggleChildren(QTreeWidgetItem *parent)
{
if (!parent)
return;
bool on = parent->checkState(0) == Qt::Checked;
for (int index = 0; index < parent->childCount(); ++index) {
parent->child(index)->setCheckState(0, on ? Qt::Checked : Qt::Unchecked);
toggleChildren(parent->child(index));
}
}
StringSet Exif::TreeView::selected()
{
StringSet result;
for (QTreeWidgetItemIterator it(this); *it; ++it) {
if ((*it)->checkState(0) == Qt::Checked)
result.insert((*it)->text(1));
}
return result;
}
void Exif::TreeView::setSelectedExif(const StringSet &selected)
{
for (QTreeWidgetItemIterator it(this); *it; ++it) {
bool on = selected.contains((*it)->text(1));
(*it)->setCheckState(0, on ? Qt::Checked : Qt::Unchecked);
}
}
void Exif::TreeView::reload()
{
clear();
setRootIsDecorated(true);
QStringList keys = Exif::Info::instance()->availableKeys().toList();
keys.sort();
QMap tree;
for (QStringList::const_iterator keysIt = keys.constBegin(); keysIt != keys.constEnd(); ++keysIt) {
QStringList subKeys = (*keysIt).split(QLatin1String("."));
QTreeWidgetItem *parent = nullptr;
QString path;
- Q_FOREACH (const QString &subKey, subKeys) {
+ for (const QString &subKey : subKeys) {
if (!path.isEmpty())
path += QString::fromLatin1(".");
path += subKey;
if (tree.contains(path))
parent = tree[path];
else {
if (parent == nullptr)
parent = new QTreeWidgetItem(this, QStringList(subKey));
else
parent = new QTreeWidgetItem(parent, QStringList(subKey));
parent->setText(1, path); // This is simply to make the implementation of selected easier.
parent->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
parent->setCheckState(0, Qt::Unchecked);
tree.insert(path, parent);
}
}
}
if (QTreeWidgetItem *item = topLevelItem(0))
item->setExpanded(true);
}
// vi:expandtab:tabstop=4 shiftwidth=4:
diff --git a/ImageManager/ThumbnailCache.cpp b/ImageManager/ThumbnailCache.cpp
index cdedeff4..1f2c3c6c 100644
--- a/ImageManager/ThumbnailCache.cpp
+++ b/ImageManager/ThumbnailCache.cpp
@@ -1,480 +1,482 @@
-/* Copyright (C) 2003-2019 The KPhotoAlbum Development Team
+/* Copyright (C) 2003-2020 The KPhotoAlbum Development Team
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.
+ 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) version 3 or any later version
+ accepted by the membership of KDE e. V. (or its successor approved
+ by the membership of KDE e. V.), which shall act as a proxy
+ defined in Section 14 of version 3 of the license.
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.
+ 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; see the file COPYING. If not, write to
- the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
- Boston, MA 02110-1301, USA.
+ along with this program. If not, see .
*/
+
#include "ThumbnailCache.h"
#include "Logging.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace
{
// We split the thumbnails into chunks to avoid a huge file changing over and over again, with a bad hit for backups
constexpr int MAX_FILE_SIZE = 32 * 1024 * 1024;
constexpr int THUMBNAIL_FILE_VERSION = 4;
// We map some thumbnail files into memory and manage them in a least-recently-used fashion
constexpr size_t LRU_SIZE = 2;
constexpr int THUMBNAIL_CACHE_SAVE_INTERNAL_MS = (5 * 1000);
}
namespace ImageManager
{
/**
* The ThumbnailMapping wraps the memory-mapped data of a QFile.
* Upon initialization with a file name, the corresponding file is opened
* and its contents mapped into memory (as a QByteArray).
*
* Deleting the ThumbnailMapping unmaps the memory and closes the file.
*/
class ThumbnailMapping
{
public:
ThumbnailMapping(const QString &filename)
: file(filename)
, map(nullptr)
{
if (!file.open(QIODevice::ReadOnly))
qCWarning(ImageManagerLog, "Failed to open thumbnail file");
uchar *data = file.map(0, file.size());
if (!data || QFile::NoError != file.error()) {
qCWarning(ImageManagerLog, "Failed to map thumbnail file");
} else {
map = QByteArray::fromRawData(reinterpret_cast(data), file.size());
}
}
bool isValid()
{
return !map.isEmpty();
}
// we need to keep the file around to keep the data mapped:
QFile file;
QByteArray map;
};
}
ImageManager::ThumbnailCache *ImageManager::ThumbnailCache::s_instance = nullptr;
ImageManager::ThumbnailCache::ThumbnailCache()
: m_currentFile(0)
, m_currentOffset(0)
, m_timer(new QTimer)
, m_needsFullSave(true)
, m_isDirty(false)
, m_memcache(new QCache(LRU_SIZE))
, m_currentWriter(nullptr)
{
const QString dir = thumbnailPath(QString());
if (!QFile::exists(dir))
QDir().mkpath(dir);
load();
connect(this, &ImageManager::ThumbnailCache::doSave, this, &ImageManager::ThumbnailCache::saveImpl);
connect(m_timer, &QTimer::timeout, this, &ImageManager::ThumbnailCache::saveImpl);
m_timer->setInterval(THUMBNAIL_CACHE_SAVE_INTERNAL_MS);
m_timer->setSingleShot(true);
m_timer->start(THUMBNAIL_CACHE_SAVE_INTERNAL_MS);
}
ImageManager::ThumbnailCache::~ThumbnailCache()
{
m_needsFullSave = true;
saveInternal();
delete m_memcache;
delete m_timer;
if (m_currentWriter)
delete m_currentWriter;
}
void ImageManager::ThumbnailCache::insert(const DB::FileName &name, const QImage &image)
{
QMutexLocker thumbnailLocker(&m_thumbnailWriterLock);
if (!m_currentWriter) {
m_currentWriter = new QFile(fileNameForIndex(m_currentFile));
if (!m_currentWriter->open(QIODevice::ReadWrite)) {
qCWarning(ImageManagerLog, "Failed to open thumbnail file for inserting");
return;
}
}
if (!m_currentWriter->seek(m_currentOffset)) {
qCWarning(ImageManagerLog, "Failed to seek in thumbnail file");
return;
}
QMutexLocker dataLocker(&m_dataLock);
// purge in-memory cache for the current file:
m_memcache->remove(m_currentFile);
QByteArray data;
QBuffer buffer(&data);
bool OK = buffer.open(QIODevice::WriteOnly);
Q_ASSERT(OK);
Q_UNUSED(OK);
OK = image.save(&buffer, "JPG");
Q_ASSERT(OK);
const int size = data.size();
if (!(m_currentWriter->write(data.data(), size) == size && m_currentWriter->flush())) {
qCWarning(ImageManagerLog, "Failed to write image data to thumbnail file");
return;
}
if (m_currentOffset + size > MAX_FILE_SIZE) {
delete m_currentWriter;
m_currentWriter = nullptr;
}
thumbnailLocker.unlock();
if (m_hash.contains(name)) {
CacheFileInfo info = m_hash[name];
if (info.fileIndex == m_currentFile && info.offset == m_currentOffset && info.size == size) {
qCDebug(ImageManagerLog) << "Found duplicate thumbnail " << name.relative() << "but no change in information";
dataLocker.unlock();
return;
} else {
// File has moved; incremental save does no good.
qCDebug(ImageManagerLog) << "Found duplicate thumbnail " << name.relative() << " at new location, need full save! ";
m_saveLock.lock();
m_needsFullSave = true;
m_saveLock.unlock();
}
}
m_hash.insert(name, CacheFileInfo(m_currentFile, m_currentOffset, size));
m_isDirty = true;
m_unsavedHash.insert(name, CacheFileInfo(m_currentFile, m_currentOffset, size));
// Update offset
m_currentOffset += size;
if (m_currentOffset > MAX_FILE_SIZE) {
m_currentFile++;
m_currentOffset = 0;
}
int unsaved = m_unsavedHash.count();
dataLocker.unlock();
// Thumbnail building is a lot faster now. Even on an HDD this corresponds to less
// than 1 minute of work.
//
// We need to call the internal version that does not interact with the timer.
// We can't simply signal from here because if we're in the middle of loading new
// images the signal won't get invoked until we return to the main application loop.
if (unsaved >= 100) {
saveInternal();
}
}
QString ImageManager::ThumbnailCache::fileNameForIndex(int index, const QString dir) const
{
return thumbnailPath(QString::fromLatin1("thumb-") + QString::number(index), dir);
}
QPixmap ImageManager::ThumbnailCache::lookup(const DB::FileName &name) const
{
m_dataLock.lock();
CacheFileInfo info = m_hash[name];
m_dataLock.unlock();
ThumbnailMapping *t = m_memcache->object(info.fileIndex);
if (!t || !t->isValid()) {
t = new ThumbnailMapping(fileNameForIndex(info.fileIndex));
if (!t->isValid()) {
qCWarning(ImageManagerLog, "Failed to map thumbnail file");
return QPixmap();
}
m_memcache->insert(info.fileIndex, t);
}
QByteArray array(t->map.mid(info.offset, info.size));
QBuffer buffer(&array);
buffer.open(QIODevice::ReadOnly);
QImage image;
image.load(&buffer, "JPG");
// Notice the above image is sharing the bits with the file, so I can't just return it as it then will be invalid when the file goes out of scope.
// PENDING(blackie) Is that still true?
return QPixmap::fromImage(image);
}
QByteArray ImageManager::ThumbnailCache::lookupRawData(const DB::FileName &name) const
{
m_dataLock.lock();
CacheFileInfo info = m_hash[name];
m_dataLock.unlock();
ThumbnailMapping *t = m_memcache->object(info.fileIndex);
if (!t || !t->isValid()) {
t = new ThumbnailMapping(fileNameForIndex(info.fileIndex));
if (!t->isValid()) {
qCWarning(ImageManagerLog, "Failed to map thumbnail file");
return QByteArray();
}
m_memcache->insert(info.fileIndex, t);
}
QByteArray array(t->map.mid(info.offset, info.size));
return array;
}
void ImageManager::ThumbnailCache::saveFull() const
{
// First ensure that any dirty thumbnails are written to disk
m_thumbnailWriterLock.lock();
if (m_currentWriter) {
delete m_currentWriter;
m_currentWriter = nullptr;
}
m_thumbnailWriterLock.unlock();
QMutexLocker dataLocker(&m_dataLock);
if (!m_isDirty) {
return;
}
QTemporaryFile file;
if (!file.open()) {
qCWarning(ImageManagerLog, "Failed to create temporary file");
return;
}
QHash tempHash = m_hash;
m_unsavedHash.clear();
m_needsFullSave = false;
// Clear the dirty flag early so that we can allow further work to proceed.
// If the save fails, we'll set the dirty flag again.
m_isDirty = false;
dataLocker.unlock();
QDataStream stream(&file);
stream << THUMBNAIL_FILE_VERSION
<< m_currentFile
<< m_currentOffset
<< m_hash.count();
for (auto it = tempHash.constBegin(); it != tempHash.constEnd(); ++it) {
const CacheFileInfo &cacheInfo = it.value();
stream << it.key().relative()
<< cacheInfo.fileIndex
<< cacheInfo.offset
<< cacheInfo.size;
}
file.close();
const QString realFileName = thumbnailPath(QString::fromLatin1("thumbnailindex"));
QFile::remove(realFileName);
if (!file.copy(realFileName)) {
qCWarning(ImageManagerLog, "Failed to copy the temporary file %s to %s", qPrintable(file.fileName()), qPrintable(realFileName));
dataLocker.relock();
m_isDirty = true;
m_needsFullSave = true;
} else {
QFile realFile(realFileName);
realFile.open(QIODevice::ReadOnly);
realFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::WriteGroup | QFile::ReadOther);
realFile.close();
}
}
// Incremental save does *not* clear the dirty flag. We always want to do a full
// save eventually.
void ImageManager::ThumbnailCache::saveIncremental() const
{
m_thumbnailWriterLock.lock();
if (m_currentWriter) {
delete m_currentWriter;
m_currentWriter = nullptr;
}
m_thumbnailWriterLock.unlock();
QMutexLocker dataLocker(&m_dataLock);
if (m_unsavedHash.count() == 0) {
return;
}
QHash tempUnsavedHash = m_unsavedHash;
m_unsavedHash.clear();
m_isDirty = true;
const QString realFileName = thumbnailPath(QString::fromLatin1("thumbnailindex"));
QFile file(realFileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Append)) {
qCWarning(ImageManagerLog, "Failed to open thumbnail cache for appending");
m_needsFullSave = true;
return;
}
QDataStream stream(&file);
for (auto it = tempUnsavedHash.constBegin(); it != tempUnsavedHash.constEnd(); ++it) {
const CacheFileInfo &cacheInfo = it.value();
stream << it.key().relative()
<< cacheInfo.fileIndex
<< cacheInfo.offset
<< cacheInfo.size;
}
file.close();
}
void ImageManager::ThumbnailCache::saveInternal() const
{
m_saveLock.lock();
const QString realFileName = thumbnailPath(QString::fromLatin1("thumbnailindex"));
// If something has asked for a full save, do it!
if (m_needsFullSave || !QFile(realFileName).exists()) {
saveFull();
} else {
saveIncremental();
}
m_saveLock.unlock();
}
void ImageManager::ThumbnailCache::saveImpl() const
{
m_timer->stop();
saveInternal();
m_timer->setInterval(THUMBNAIL_CACHE_SAVE_INTERNAL_MS);
m_timer->setSingleShot(true);
m_timer->start(THUMBNAIL_CACHE_SAVE_INTERNAL_MS);
}
void ImageManager::ThumbnailCache::save() const
{
m_saveLock.lock();
m_needsFullSave = true;
m_saveLock.unlock();
emit doSave();
}
void ImageManager::ThumbnailCache::load()
{
QFile file(thumbnailPath(QString::fromLatin1("thumbnailindex")));
if (!file.exists())
return;
QElapsedTimer timer;
timer.start();
file.open(QIODevice::ReadOnly);
QDataStream stream(&file);
int version;
stream >> version;
if (version != THUMBNAIL_FILE_VERSION)
return; //Discard cache
// We can't allow anything to modify the structure while we're doing this.
QMutexLocker dataLocker(&m_dataLock);
int count = 0;
stream >> m_currentFile
>> m_currentOffset
>> count;
while (!stream.atEnd()) {
QString name;
int fileIndex;
int offset;
int size;
stream >> name
>> fileIndex
>> offset
>> size;
m_hash.insert(DB::FileName::fromRelativePath(name), CacheFileInfo(fileIndex, offset, size));
if (fileIndex > m_currentFile) {
m_currentFile = fileIndex;
m_currentOffset = offset + size;
} else if (fileIndex == m_currentFile && offset + size > m_currentOffset) {
m_currentOffset = offset + size;
}
if (m_currentOffset > MAX_FILE_SIZE) {
m_currentFile++;
m_currentOffset = 0;
}
count++;
}
qCDebug(TimingLog) << "Loaded thumbnails in " << timer.elapsed() / 1000.0 << " seconds";
}
bool ImageManager::ThumbnailCache::contains(const DB::FileName &name) const
{
QMutexLocker dataLocker(&m_dataLock);
bool answer = m_hash.contains(name);
return answer;
}
QString ImageManager::ThumbnailCache::thumbnailPath(const QString &file, const QString dir) const
{
QString base = QDir(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath(dir);
return base + file;
}
ImageManager::ThumbnailCache *ImageManager::ThumbnailCache::instance()
{
if (!s_instance) {
s_instance = new ThumbnailCache;
}
return s_instance;
}
void ImageManager::ThumbnailCache::deleteInstance()
{
delete s_instance;
s_instance = nullptr;
}
void ImageManager::ThumbnailCache::flush()
{
QMutexLocker dataLocker(&m_dataLock);
for (int i = 0; i <= m_currentFile; ++i)
QFile::remove(fileNameForIndex(i));
m_currentFile = 0;
m_currentOffset = 0;
m_isDirty = true;
m_hash.clear();
m_unsavedHash.clear();
m_memcache->clear();
dataLocker.unlock();
save();
}
void ImageManager::ThumbnailCache::removeThumbnail(const DB::FileName &fileName)
{
QMutexLocker dataLocker(&m_dataLock);
m_isDirty = true;
m_hash.remove(fileName);
dataLocker.unlock();
save();
}
void ImageManager::ThumbnailCache::removeThumbnails(const DB::FileNameList &files)
{
QMutexLocker dataLocker(&m_dataLock);
m_isDirty = true;
- Q_FOREACH (const DB::FileName &fileName, files) {
+ for (const DB::FileName &fileName : files) {
m_hash.remove(fileName);
}
dataLocker.unlock();
save();
}
// vi:expandtab:tabstop=4 shiftwidth=4:
diff --git a/ImportExport/ImportDialog.cpp b/ImportExport/ImportDialog.cpp
index bafdcc30..1192452e 100644
--- a/ImportExport/ImportDialog.cpp
+++ b/ImportExport/ImportDialog.cpp
@@ -1,394 +1,395 @@
-/* Copyright (C) 2003-2019 The KPhotoAlbum Development Team
+/* Copyright (C) 2003-2020 The KPhotoAlbum Development Team
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.
+ 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) version 3 or any later version
+ accepted by the membership of KDE e. V. (or its successor approved
+ by the membership of KDE e. V.), which shall act as a proxy
+ defined in Section 14 of version 3 of the license.
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.
+ 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; see the file COPYING. If not, write to
- the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
- Boston, MA 02110-1301, USA.
+ along with this program. If not, see .
*/
#include "ImportDialog.h"
#include "ImageRow.h"
#include "ImportMatcher.h"
#include "KimFileReader.h"
#include "MD5CheckPage.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using Utilities::StringSet;
class QPushButton;
using namespace ImportExport;
ImportDialog::ImportDialog(QWidget *parent)
: KAssistantDialog(parent)
, m_hasFilled(false)
, m_md5CheckPage(nullptr)
{
}
bool ImportDialog::exec(KimFileReader *kimFileReader, const QUrl &kimFileURL)
{
m_kimFileReader = kimFileReader;
if (kimFileURL.isLocalFile()) {
QDir cwd;
// convert relative local path to absolute
m_kimFile = QUrl::fromLocalFile(cwd.absoluteFilePath(kimFileURL.toLocalFile()))
.adjusted(QUrl::NormalizePathSegments);
} else {
m_kimFile = kimFileURL;
}
QByteArray indexXML = m_kimFileReader->indexXML();
if (indexXML.isNull())
return false;
bool ok = readFile(indexXML);
if (!ok)
return false;
setupPages();
return KAssistantDialog::exec();
}
bool ImportDialog::readFile(const QByteArray &data)
{
XMLDB::ReaderPtr reader = XMLDB::ReaderPtr(new XMLDB::XmlReader(DB::ImageDB::instance()->uiDelegate(),
m_kimFile.toDisplayString()));
reader->addData(data);
XMLDB::ElementInfo info = reader->readNextStartOrStopElement(QString::fromUtf8("KimDaBa-export"));
if (!info.isStartToken)
reader->complainStartElementExpected(QString::fromUtf8("KimDaBa-export"));
// Read source
QString source = reader->attribute(QString::fromUtf8("location")).toLower();
if (source != QString::fromLatin1("inline") && source != QString::fromLatin1("external")) {
KMessageBox::error(this, i18n("XML file did not specify the source of the images, "
"this is a strong indication that the file is corrupted
"));
return false;
}
m_externalSource = (source == QString::fromLatin1("external"));
// Read base url
m_baseUrl = QUrl::fromUserInput(reader->attribute(QString::fromLatin1("baseurl")));
while (reader->readNextStartOrStopElement(QString::fromUtf8("image")).isStartToken) {
const DB::FileName fileName = DB::FileName::fromRelativePath(reader->attribute(QString::fromUtf8("file")));
DB::ImageInfoPtr info = XMLDB::Database::createImageInfo(fileName, reader);
m_images.append(info);
}
// the while loop already read the end element, so we tell readEndElement to not read the next token:
reader->readEndElement(false);
return true;
}
void ImportDialog::setupPages()
{
createIntroduction();
createImagesPage();
createDestination();
createCategoryPages();
connect(this, &ImportDialog::currentPageChanged, this, &ImportDialog::updateNextButtonState);
QPushButton *helpButton = buttonBox()->button(QDialogButtonBox::Help);
connect(helpButton, &QPushButton::clicked, this, &ImportDialog::slotHelp);
}
void ImportDialog::createIntroduction()
{
QString txt = i18n("Welcome to KPhotoAlbum Import
"
"This wizard will take you through the steps of an import operation. The steps are: "
"- First you must select which images you want to import from the export file. "
"You do so by selecting the checkbox next to the image.
"
"- Next you must tell KPhotoAlbum in which directory to put the images. This directory must "
"of course be below the directory root KPhotoAlbum uses for images. "
"KPhotoAlbum will take care to avoid name clashes
"
"- The next step is to specify which categories you want to import (People, Places, ... ) "
"and also tell KPhotoAlbum how to match the categories from the file to your categories. "
"Imagine you load from a file, where a category is called Blomst (which is the "
"Danish word for flower), then you would likely want to match this with your category, which might be "
"called Blume (which is the German word for flower) - of course given you are German.
"
"- The final steps, is matching the individual tokens from the categories. I may call myself Jesper "
"in my image database, while you want to call me by my full name, namely Jesper K. Pedersen. "
"In this step non matches will be highlighted in red, so you can see which tokens was not found in your "
"database, or which tokens was only a partial match.
");
QLabel *intro = new QLabel(txt, this);
intro->setWordWrap(true);
addPage(intro, i18nc("@title:tab introduction page", "Introduction"));
}
void ImportDialog::createImagesPage()
{
QScrollArea *top = new QScrollArea;
top->setWidgetResizable(true);
QWidget *container = new QWidget;
QVBoxLayout *lay1 = new QVBoxLayout(container);
top->setWidget(container);
// Select all and Deselect All buttons
QHBoxLayout *lay2 = new QHBoxLayout;
lay1->addLayout(lay2);
QPushButton *selectAll = new QPushButton(i18n("Select All"), container);
lay2->addWidget(selectAll);
QPushButton *selectNone = new QPushButton(i18n("Deselect All"), container);
lay2->addWidget(selectNone);
lay2->addStretch(1);
connect(selectAll, &QPushButton::clicked, this, &ImportDialog::slotSelectAll);
connect(selectNone, &QPushButton::clicked, this, &ImportDialog::slotSelectNone);
QGridLayout *lay3 = new QGridLayout;
lay1->addLayout(lay3);
lay3->setColumnStretch(2, 1);
int row = 0;
for (DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it, ++row) {
DB::ImageInfoPtr info = *it;
ImageRow *ir = new ImageRow(info, this, m_kimFileReader, container);
lay3->addWidget(ir->m_checkbox, row, 0);
QPixmap pixmap = m_kimFileReader->loadThumbnail(info->fileName().relative());
if (!pixmap.isNull()) {
QPushButton *but = new QPushButton(container);
but->setIcon(pixmap);
but->setIconSize(pixmap.size());
lay3->addWidget(but, row, 1);
connect(but, &QPushButton::clicked, ir, &ImageRow::showImage);
} else {
QLabel *label = new QLabel(info->label());
lay3->addWidget(label, row, 1);
}
QLabel *label = new QLabel(QString::fromLatin1("%1
").arg(info->description()));
lay3->addWidget(label, row, 2);
m_imagesSelect.append(ir);
}
addPage(top, i18n("Select Which Images to Import"));
}
void ImportDialog::createDestination()
{
QWidget *top = new QWidget(this);
QVBoxLayout *topLay = new QVBoxLayout(top);
QHBoxLayout *lay = new QHBoxLayout;
topLay->addLayout(lay);
topLay->addStretch(1);
QLabel *label = new QLabel(i18n("Destination of images: "), top);
lay->addWidget(label);
m_destinationEdit = new QLineEdit(top);
lay->addWidget(m_destinationEdit, 1);
QPushButton *but = new QPushButton(QString::fromLatin1("..."), top);
but->setFixedWidth(30);
lay->addWidget(but);
m_destinationEdit->setText(Settings::SettingsData::instance()->imageDirectory());
connect(but, &QPushButton::clicked, this, &ImportDialog::slotEditDestination);
connect(m_destinationEdit, &QLineEdit::textChanged, this, &ImportDialog::updateNextButtonState);
m_destinationPage = addPage(top, i18n("Destination of Images"));
}
void ImportDialog::slotEditDestination()
{
QString file = QFileDialog::getExistingDirectory(this, QString(), m_destinationEdit->text());
if (!file.isNull()) {
if (!QFileInfo(file).absoluteFilePath().startsWith(QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath())) {
KMessageBox::error(this, i18n("The directory must be a subdirectory of %1", Settings::SettingsData::instance()->imageDirectory()));
} else if (QFileInfo(file).absoluteFilePath().startsWith(
QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath() + QString::fromLatin1("CategoryImages"))) {
KMessageBox::error(this, i18n("This directory is reserved for category images."));
} else {
m_destinationEdit->setText(file);
updateNextButtonState();
}
}
}
void ImportDialog::updateNextButtonState()
{
bool enabled = true;
if (currentPage() == m_destinationPage) {
QString dest = m_destinationEdit->text();
if (QFileInfo(dest).isFile())
enabled = false;
else if (!QFileInfo(dest).absoluteFilePath().startsWith(QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath()))
enabled = false;
}
setValid(currentPage(), enabled);
}
void ImportDialog::createCategoryPages()
{
QStringList categories;
DB::ImageInfoList images = selectedImages();
for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) {
DB::ImageInfoPtr info = *it;
QStringList categoriesForImage = info->availableCategories();
- Q_FOREACH (const QString &category, categoriesForImage) {
+ for (const QString &category : categoriesForImage) {
if (!categories.contains(category) && category != i18n("Folder") && category != i18n("Tokens") && category != i18n("Media Type"))
categories.append(category);
}
}
if (!categories.isEmpty()) {
m_categoryMatcher = new ImportMatcher(QString(), QString(), categories, DB::ImageDB::instance()->categoryCollection()->categoryNames(),
false, this);
m_categoryMatcherPage = addPage(m_categoryMatcher, i18n("Match Categories"));
QWidget *dummy = new QWidget;
m_dummy = addPage(dummy, QString());
} else {
m_categoryMatcherPage = nullptr;
possiblyAddMD5CheckPage();
}
}
ImportMatcher *ImportDialog::createCategoryPage(const QString &myCategory, const QString &otherCategory)
{
StringSet otherItems;
DB::ImageInfoList images = selectedImages();
for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) {
otherItems += (*it)->itemsOfCategory(otherCategory);
}
QStringList myItems = DB::ImageDB::instance()->categoryCollection()->categoryForName(myCategory)->itemsInclCategories();
myItems.sort();
ImportMatcher *matcher = new ImportMatcher(otherCategory, myCategory, otherItems.toList(), myItems, true, this);
addPage(matcher, myCategory);
return matcher;
}
void ImportDialog::next()
{
if (currentPage() == m_destinationPage) {
QString dir = m_destinationEdit->text();
if (!QFileInfo(dir).exists()) {
int answer = KMessageBox::questionYesNo(this, i18n("Directory %1 does not exist. Should it be created?", dir));
if (answer == KMessageBox::Yes) {
bool ok = QDir().mkpath(dir);
if (!ok) {
KMessageBox::error(this, i18n("Error creating directory %1", dir));
return;
}
} else
return;
}
}
if (!m_hasFilled && currentPage() == m_categoryMatcherPage) {
m_hasFilled = true;
m_categoryMatcher->setEnabled(false);
removePage(m_dummy);
ImportMatcher *matcher = nullptr;
- Q_FOREACH (const CategoryMatch *match, m_categoryMatcher->m_matchers) {
+ for (const CategoryMatch *match : m_categoryMatcher->m_matchers) {
if (match->m_checkbox->isChecked()) {
matcher = createCategoryPage(match->m_combobox->currentText(), match->m_text);
m_matchers.append(matcher);
}
}
possiblyAddMD5CheckPage();
}
KAssistantDialog::next();
}
void ImportDialog::slotSelectAll()
{
selectImage(true);
}
void ImportDialog::slotSelectNone()
{
selectImage(false);
}
void ImportDialog::selectImage(bool on)
{
- Q_FOREACH (ImageRow *row, m_imagesSelect) {
+ for (ImageRow *row : m_imagesSelect) {
row->m_checkbox->setChecked(on);
}
}
DB::ImageInfoList ImportDialog::selectedImages() const
{
DB::ImageInfoList res;
for (QList::ConstIterator it = m_imagesSelect.begin(); it != m_imagesSelect.end(); ++it) {
if ((*it)->m_checkbox->isChecked())
res.append((*it)->m_info);
}
return res;
}
void ImportDialog::slotHelp()
{
KHelpClient::invokeHelp(QString::fromLatin1("chp-importExport"));
}
ImportSettings ImportExport::ImportDialog::settings()
{
ImportSettings settings;
settings.setSelectedImages(selectedImages());
settings.setDestination(m_destinationEdit->text());
settings.setExternalSource(m_externalSource);
settings.setKimFile(m_kimFile);
settings.setBaseURL(m_baseUrl);
if (m_md5CheckPage) {
settings.setImportActions(m_md5CheckPage->settings());
}
for (ImportMatcher *match : m_matchers)
settings.addCategoryMatchSetting(match->settings());
return settings;
}
void ImportExport::ImportDialog::possiblyAddMD5CheckPage()
{
if (MD5CheckPage::pageNeeded(settings())) {
m_md5CheckPage = new MD5CheckPage(settings());
addPage(m_md5CheckPage, i18n("How to resolve clashes"));
}
}
// vi:expandtab:tabstop=4 shiftwidth=4:
diff --git a/ImportExport/ImportHandler.cpp b/ImportExport/ImportHandler.cpp
index 91beb323..b773f60c 100644
--- a/ImportExport/ImportHandler.cpp
+++ b/ImportExport/ImportHandler.cpp
@@ -1,349 +1,351 @@
-/* Copyright (C) 2003-2010 Jesper K. Pedersen
+/* Copyright (C) 2003-2020 The KPhotoAlbum Development Team
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.
+ 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) version 3 or any later version
+ accepted by the membership of KDE e. V. (or its successor approved
+ by the membership of KDE e. V.), which shall act as a proxy
+ defined in Section 14 of version 3 of the license.
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.
+ 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; see the file COPYING. If not, write to
- the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
- Boston, MA 02110-1301, USA.
+ along with this program. If not, see .
*/
+
#include "ImportHandler.h"
#include "ImportSettings.h"
#include "KimFileReader.h"
#include "Logging.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace ImportExport;
ImportExport::ImportHandler::ImportHandler()
: m_fileMapper(nullptr)
, m_finishedPressed(false)
, m_progress(0)
, m_reportUnreadableFiles(true)
, m_eventLoop(new QEventLoop)
{
}
ImportHandler::~ImportHandler()
{
delete m_fileMapper;
delete m_eventLoop;
}
bool ImportExport::ImportHandler::exec(const ImportSettings &settings, KimFileReader *kimFileReader)
{
m_settings = settings;
m_kimFileReader = kimFileReader;
m_finishedPressed = true;
delete m_fileMapper;
m_fileMapper = new Utilities::UniqFilenameMapper(m_settings.destination());
bool ok;
// copy images
if (m_settings.externalSource()) {
copyFromExternal();
// If none of the images were to be copied, then we flushed the loop before we got started, in that case, don't start the loop.
qCDebug(ImportExportLog) << "Copying" << m_pendingCopies.count() << "files from external source...";
if (m_pendingCopies.count() > 0)
ok = m_eventLoop->exec();
else
ok = false;
} else {
ok = copyFilesFromZipFile();
if (ok)
updateDB();
}
if (m_progress)
delete m_progress;
return ok;
}
void ImportExport::ImportHandler::copyFromExternal()
{
m_pendingCopies = m_settings.selectedImages();
m_totalCopied = 0;
m_progress = new QProgressDialog(MainWindow::Window::theMainWindow());
m_progress->setWindowTitle(i18nc("@title:window", "Copying Images"));
m_progress->setMinimum(0);
m_progress->setMaximum(2 * m_pendingCopies.count());
m_progress->show();
connect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages);
copyNextFromExternal();
}
void ImportExport::ImportHandler::copyNextFromExternal()
{
DB::ImageInfoPtr info = m_pendingCopies[0];
if (isImageAlreadyInDB(info)) {
qCDebug(ImportExportLog) << info->fileName().relative() << "is already in database.";
aCopyJobCompleted(0);
return;
}
const DB::FileName fileName = info->fileName();
bool succeeded = false;
QStringList tried;
// First search for images next to the .kim file
// Second search for images base on the image root as specified in the .kim file
QList searchUrls {
m_settings.kimFile().adjusted(QUrl::RemoveFilename), m_settings.baseURL().adjusted(QUrl::RemoveFilename)
};
- Q_FOREACH (const QUrl &url, searchUrls) {
+ for (const QUrl &url : searchUrls) {
QUrl src(url);
src.setPath(src.path() + fileName.relative());
std::unique_ptr statJob { KIO::stat(src, KIO::StatJob::SourceSide, 0 /* just query for existence */) };
KJobWidgets::setWindow(statJob.get(), MainWindow::Window::theMainWindow());
if (statJob->exec()) {
QUrl dest = QUrl::fromLocalFile(m_fileMapper->uniqNameFor(fileName));
m_job = KIO::file_copy(src, dest, -1, KIO::HideProgressInfo);
connect(m_job, &KIO::FileCopyJob::result, this, &ImportHandler::aCopyJobCompleted);
succeeded = true;
qCDebug(ImportExportLog) << "Copying" << src << "to" << dest;
break;
} else
tried << src.toDisplayString();
}
if (!succeeded)
aCopyFailed(tried);
}
bool ImportExport::ImportHandler::copyFilesFromZipFile()
{
DB::ImageInfoList images = m_settings.selectedImages();
m_totalCopied = 0;
m_progress = new QProgressDialog(MainWindow::Window::theMainWindow());
m_progress->setWindowTitle(i18nc("@title:window", "Copying Images"));
m_progress->setMinimum(0);
m_progress->setMaximum(2 * m_pendingCopies.count());
m_progress->show();
for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) {
if (!isImageAlreadyInDB(*it)) {
const DB::FileName fileName = (*it)->fileName();
QByteArray data = m_kimFileReader->loadImage(fileName.relative());
if (data.isNull())
return false;
QString newName = m_fileMapper->uniqNameFor(fileName);
QFile out(newName);
if (!out.open(QIODevice::WriteOnly)) {
KMessageBox::error(MainWindow::Window::theMainWindow(), i18n("Error when writing image %1", newName));
return false;
}
out.write(data.constData(), data.size());
out.close();
}
qApp->processEvents();
m_progress->setValue(++m_totalCopied);
if (m_progress->wasCanceled()) {
return false;
}
}
return true;
}
void ImportExport::ImportHandler::updateDB()
{
disconnect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages);
m_progress->setLabelText(i18n("Updating Database"));
int len = Settings::SettingsData::instance()->imageDirectory().length();
// image directory is always a prefix of destination
if (len == m_settings.destination().length())
len = 0;
else
qCDebug(ImportExportLog)
<< "Re-rooting of ImageInfos from " << Settings::SettingsData::instance()->imageDirectory()
<< " to " << m_settings.destination();
// Run though all images
DB::ImageInfoList images = m_settings.selectedImages();
for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) {
DB::ImageInfoPtr info = *it;
if (len != 0) {
// exchange prefix:
QString name = m_settings.destination() + info->fileName().absolute().mid(len);
qCDebug(ImportExportLog) << info->fileName().absolute() << " -> " << name;
info->setFileName(DB::FileName::fromAbsolutePath(name));
}
if (isImageAlreadyInDB(info)) {
qCDebug(ImportExportLog) << "Updating ImageInfo for " << info->fileName().absolute();
updateInfo(matchingInfoFromDB(info), info);
} else {
qCDebug(ImportExportLog) << "Adding ImageInfo for " << info->fileName().absolute();
addNewRecord(info);
}
m_progress->setValue(++m_totalCopied);
if (m_progress->wasCanceled())
break;
}
Browser::BrowserWidget::instance()->home();
}
void ImportExport::ImportHandler::stopCopyingImages()
{
m_job->kill();
}
void ImportExport::ImportHandler::aCopyFailed(QStringList files)
{
int result = m_reportUnreadableFiles ? KMessageBox::warningYesNoCancelList(m_progress,
i18n("Cannot copy from any of the following locations:"),
files, QString(), KStandardGuiItem::cont(), KGuiItem(i18n("Continue without Asking")))
: KMessageBox::Yes;
switch (result) {
case KMessageBox::Cancel:
// This might be late -- if we managed to copy some files, we will
// just throw away any changes to the DB, but some new image files
// might be in the image directory...
m_eventLoop->exit(false);
m_pendingCopies.pop_front();
break;
case KMessageBox::No:
m_reportUnreadableFiles = false;
// fall through
default:
aCopyJobCompleted(0);
}
}
void ImportExport::ImportHandler::aCopyJobCompleted(KJob *job)
{
qCDebug(ImportExportLog) << "CopyJob" << job << "completed.";
m_pendingCopies.pop_front();
if (job && job->error()) {
job->uiDelegate()->showErrorMessage();
m_eventLoop->exit(false);
} else if (m_pendingCopies.count() == 0) {
updateDB();
m_eventLoop->exit(true);
} else if (m_progress->wasCanceled()) {
m_eventLoop->exit(false);
} else {
m_progress->setValue(++m_totalCopied);
copyNextFromExternal();
}
}
bool ImportExport::ImportHandler::isImageAlreadyInDB(const DB::ImageInfoPtr &info)
{
return DB::ImageDB::instance()->md5Map()->contains(info->MD5Sum());
}
DB::ImageInfoPtr ImportExport::ImportHandler::matchingInfoFromDB(const DB::ImageInfoPtr &info)
{
const DB::FileName name = DB::ImageDB::instance()->md5Map()->lookup(info->MD5Sum());
return DB::ImageDB::instance()->info(name);
}
/**
* Merge the ImageInfo data from the kim file into the existing ImageInfo.
*/
void ImportExport::ImportHandler::updateInfo(DB::ImageInfoPtr dbInfo, DB::ImageInfoPtr newInfo)
{
if (dbInfo->label() != newInfo->label() && m_settings.importAction(QString::fromLatin1("*Label*")) == ImportSettings::Replace)
dbInfo->setLabel(newInfo->label());
if (dbInfo->description().simplified() != newInfo->description().simplified()) {
if (m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Replace)
dbInfo->setDescription(newInfo->description());
else if (m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Merge)
dbInfo->setDescription(dbInfo->description() + QString::fromLatin1("
") + newInfo->description());
}
if (dbInfo->angle() != newInfo->angle() && m_settings.importAction(QString::fromLatin1("*Orientation*")) == ImportSettings::Replace)
dbInfo->setAngle(newInfo->angle());
if (dbInfo->date() != newInfo->date() && m_settings.importAction(QString::fromLatin1("*Date*")) == ImportSettings::Replace)
dbInfo->setDate(newInfo->date());
updateCategories(newInfo, dbInfo, false);
}
void ImportExport::ImportHandler::addNewRecord(DB::ImageInfoPtr info)
{
const DB::FileName importName = info->fileName();
DB::ImageInfoPtr updateInfo(new DB::ImageInfo(importName, info->mediaType(), false /*don't read exif */));
updateInfo->setLabel(info->label());
updateInfo->setDescription(info->description());
updateInfo->setDate(info->date());
updateInfo->setAngle(info->angle());
updateInfo->setMD5Sum(DB::MD5Sum(updateInfo->fileName()));
DB::ImageInfoList list;
list.append(updateInfo);
DB::ImageDB::instance()->addImages(list);
updateCategories(info, updateInfo, true);
}
void ImportExport::ImportHandler::updateCategories(DB::ImageInfoPtr XMLInfo, DB::ImageInfoPtr DBInfo, bool forceReplace)
{
// Run though the categories
const QList matches = m_settings.categoryMatchSetting();
for (const CategoryMatchSetting &match : matches) {
QString XMLCategoryName = match.XMLCategoryName();
QString DBCategoryName = match.DBCategoryName();
ImportSettings::ImportAction action = m_settings.importAction(DBCategoryName);
const Utilities::StringSet items = XMLInfo->itemsOfCategory(XMLCategoryName);
DB::CategoryPtr DBCategoryPtr = DB::ImageDB::instance()->categoryCollection()->categoryForName(DBCategoryName);
if (!forceReplace && action == ImportSettings::Replace)
DBInfo->setCategoryInfo(DBCategoryName, Utilities::StringSet());
if (action == ImportSettings::Merge || action == ImportSettings::Replace || forceReplace) {
for (const QString &item : items) {
if (match.XMLtoDB().contains(item)) {
DBInfo->addCategoryInfo(DBCategoryName, match.XMLtoDB()[item]);
DBCategoryPtr->addItem(match.XMLtoDB()[item]);
}
}
}
}
}
// vi:expandtab:tabstop=4 shiftwidth=4:
diff --git a/ImportExport/MD5CheckPage.cpp b/ImportExport/MD5CheckPage.cpp
index c1c7600f..3d524a0c 100644
--- a/ImportExport/MD5CheckPage.cpp
+++ b/ImportExport/MD5CheckPage.cpp
@@ -1,199 +1,201 @@
-/* Copyright (C) 2003-2010 Jesper K. Pedersen