diff --git a/DB/FileInfo.cpp b/DB/FileInfo.cpp index 115fa4fc..facf3f62 100644 --- a/DB/FileInfo.cpp +++ b/DB/FileInfo.cpp @@ -1,137 +1,140 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen 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. 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. 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. */ #include "FileInfo.h" + +#include #include -#include -#include -#include "Utilities/Util.h" -#include "Exif/Info.h" +#include +#include + +#include +#include using namespace DB; FileInfo FileInfo::read( const DB::FileName& fileName, DB::ExifMode mode ) { return FileInfo( fileName, mode ); } DB::FileInfo::FileInfo( const DB::FileName& fileName, DB::ExifMode mode ) : m_angle(0), m_fileName(fileName) { parseEXIV2( fileName ); if ( updateDataFromFileTimeStamp(fileName,mode)) m_date = QFileInfo( fileName.absolute() ).lastModified(); } Exiv2::ExifData& DB::FileInfo::getExifData() { return m_exifMap; } const DB::FileName& DB::FileInfo::getFileName() const { return m_fileName; } bool DB::FileInfo::updateDataFromFileTimeStamp(const DB::FileName& fileName, DB::ExifMode mode) { // If the date is valid from Exif reading, then we should not use the time stamp from the file. if ( m_date.isValid() ) return false; // If we are not setting date, then we should of course not set the date if ( (mode & EXIFMODE_DATE) == 0 ) return false; // If we are we already have specifies that we want to sent the date (from the ReReadExif dialog), then we of course should. if ( (mode & EXIFMODE_USE_IMAGE_DATE_IF_INVALID_EXIF_DATE ) != 0) return true; // Always trust for videos (this is a way to say that we should not trust for scaned in images - which makes no sense for videos) if ( Utilities::isVideo(fileName) ) return true; // Finally use the info from the settings dialog return Settings::SettingsData::instance()->trustTimeStamps(); } void DB::FileInfo::parseEXIV2( const DB::FileName& fileName ) { m_exifMap = Exif::Info::instance()->metadata( fileName ).exif; // Date m_date = fetchEXIV2Date( m_exifMap, "Exif.Photo.DateTimeOriginal" ); if ( !m_date.isValid() ) { m_date = fetchEXIV2Date( m_exifMap, "Exif.Photo.DateTimeDigitized" ); if ( !m_date.isValid() ) m_date = fetchEXIV2Date( m_exifMap, "Exif.Image.DateTime" ); } // Angle if ( m_exifMap.findKey( Exiv2::ExifKey( "Exif.Image.Orientation" ) ) != m_exifMap.end() ) { const Exiv2::Exifdatum& datum = m_exifMap["Exif.Image.Orientation"]; int orientation = 0; if (datum.count() > 0) orientation = datum.toLong(); m_angle = orientationToAngle( orientation ); } // Description if( m_exifMap.findKey( Exiv2::ExifKey( "Exif.Image.ImageDescription" ) ) != m_exifMap.end() ) { const Exiv2::Exifdatum& datum = m_exifMap["Exif.Image.ImageDescription"]; m_description = QString::fromLocal8Bit( datum.toString().c_str() ).trimmed(); // some cameras seem to add control characters. Remove them: m_description.remove(QRegularExpression(QString::fromLatin1("\\p{Cc}"))); } } QDateTime FileInfo::fetchEXIV2Date( Exiv2::ExifData& map, const char* key ) { try { if ( map.findKey( Exiv2::ExifKey( key ) ) != map.end() ) { const Exiv2::Exifdatum& datum = map[key ]; return QDateTime::fromString( QString::fromLatin1(datum.toString().c_str()), Qt::ISODate ); } } catch (...) { } return QDateTime(); } int DB::FileInfo::orientationToAngle( int orientation ) { if ( orientation == 1 || orientation == 2 ) return 0; else if ( orientation == 3 || orientation == 4 ) return 180; else if ( orientation == 5 || orientation == 8 ) return 270; else if ( orientation == 6 || orientation == 7 ) return 90; return 0; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FileName.cpp b/DB/FileName.cpp index 7b8a60a4..586c00a3 100644 --- a/DB/FileName.cpp +++ b/DB/FileName.cpp @@ -1,99 +1,103 @@ /* Copyright 2012 Jesper K. Pedersen 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) 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. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "FileName.h" +#include "ImageDB.h" + +#include #include +#include + #include -#include "ImageDB.h" DB::FileName::FileName() : m_isNull(true) { } DB::FileName DB::FileName::fromAbsolutePath(const QString &fileName) { const QString imageRoot = Utilities::stripEndingForwardSlash( Settings::SettingsData::instance()->imageDirectory() ) + QLatin1String("/"); if (!fileName.startsWith(imageRoot)) return FileName(); FileName res; res.m_isNull = false; res.m_absoluteFilePath = fileName; res.m_relativePath = fileName.mid(imageRoot.length()); return res; } DB::FileName DB::FileName::fromRelativePath(const QString &fileName) { Q_ASSERT(!fileName.startsWith(QChar::fromLatin1('/'))); FileName res; res.m_isNull = false; res.m_relativePath = fileName; res.m_absoluteFilePath = Utilities::stripEndingForwardSlash( Settings::SettingsData::instance()->imageDirectory() ) + QLatin1String("/") + fileName; return res; } QString DB::FileName::absolute() const { Q_ASSERT(!isNull()); return m_absoluteFilePath; } QString DB::FileName::relative() const { Q_ASSERT(!m_isNull); return m_relativePath; } bool DB::FileName::isNull() const { return m_isNull; } bool DB::FileName::operator ==(const DB::FileName &other) const { return m_isNull == other.m_isNull && m_relativePath == other.m_relativePath; } bool DB::FileName::operator !=(const DB::FileName &other) const { return !(*this == other); } bool DB::FileName::operator <(const DB::FileName &other) const { return relative() < other.relative(); } bool DB::FileName::exists() const { return QFile::exists(absolute()); } DB::ImageInfoPtr DB::FileName::info() const { return ImageDB::instance()->info(*this); } uint DB::qHash( const DB::FileName& fileName ) { return qHash(fileName.relative()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/AutoStackImages.cpp b/MainWindow/AutoStackImages.cpp index ae76a88e..2e8cdcbb 100644 --- a/MainWindow/AutoStackImages.cpp +++ b/MainWindow/AutoStackImages.cpp @@ -1,326 +1,327 @@ /* Copyright (C) 2010-2018 Miika Turkia 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. 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. 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. */ #include "AutoStackImages.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include using namespace MainWindow; AutoStackImages::AutoStackImages( QWidget* parent, const DB::FileNameList& list ) :QDialog( parent ), m_list( list ) { setWindowTitle( i18nc("@title:window", "Automatically Stack Images" ) ); QWidget* top = new QWidget; QVBoxLayout* lay1 = new QVBoxLayout( top ); setLayout(lay1); QWidget* containerMd5 = new QWidget( this ); lay1->addWidget( containerMd5 ); QHBoxLayout* hlayMd5 = new QHBoxLayout( containerMd5 ); m_matchingMD5 = new QCheckBox( i18n( "Stack images with identical MD5 sum") ); m_matchingMD5->setChecked( false ); hlayMd5->addWidget( m_matchingMD5 ); QWidget* containerFile = new QWidget( this ); lay1->addWidget( containerFile ); QHBoxLayout* hlayFile = new QHBoxLayout( containerFile ); m_matchingFile = new QCheckBox( i18n( "Stack images based on file version detection") ); m_matchingFile->setChecked( true ); hlayFile->addWidget( m_matchingFile ); m_origTop = new QCheckBox( i18n( "Original to top") ); m_origTop ->setChecked( false ); hlayFile->addWidget( m_origTop ); QWidget* containerContinuous = new QWidget( this ); lay1->addWidget( containerContinuous ); QHBoxLayout* hlayContinuous = new QHBoxLayout( containerContinuous ); //FIXME: This is hard to translate because of the split sentence. It is better //to use a single sentence here like "Stack images that are (were?) shot //within this time:" and use the spin method setSuffix() to set the "seconds". //Also: Would minutes not be a more sane time unit here? (schwarzer) m_continuousShooting = new QCheckBox( i18nc( "The whole sentence should read: *Stack images that are shot within x seconds of each other*. So images that are shot in one burst are automatically stacked together. (This sentence is before the x.)", "Stack images that are shot within" ) ); m_continuousShooting->setChecked( false ); hlayContinuous->addWidget( m_continuousShooting ); m_continuousThreshold = new QSpinBox; m_continuousThreshold->setRange( 1, 999 ); m_continuousThreshold->setSingleStep( 1 ); m_continuousThreshold->setValue( 2 ); hlayContinuous->addWidget( m_continuousThreshold ); QLabel* sec = new QLabel( i18nc( "The whole sentence should read: *Stack images that are shot within x seconds of each other*. (This being the text after x.)", "seconds" ), containerContinuous ); hlayContinuous->addWidget( sec ); QGroupBox* grpOptions = new QGroupBox( i18n("AutoStacking Options") ); QVBoxLayout* grpLayOptions = new QVBoxLayout( grpOptions ); lay1->addWidget( grpOptions ); m_autostackDefault = new QRadioButton( i18n( "Include matching image to appropriate stack (if one exists)") ); m_autostackDefault->setChecked( true ); grpLayOptions->addWidget( m_autostackDefault ); m_autostackUnstack = new QRadioButton( i18n( "Unstack images from their current stack and create new one for the matches") ); m_autostackUnstack->setChecked( false ); grpLayOptions->addWidget( m_autostackUnstack ); m_autostackSkip = new QRadioButton( i18n( "Skip images that are already in a stack") ); m_autostackSkip->setChecked( false ); grpLayOptions->addWidget( m_autostackSkip ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &AutoStackImages::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &AutoStackImages::reject); lay1->addWidget(buttonBox); } /* * This function searches for images with matching MD5 sums * Matches are automatically stacked */ void AutoStackImages::matchingMD5( DB::FileNameList& toBeShown ) { QMap< DB::MD5, DB::FileNameList > tostack; DB::FileNameList showIfStacked; // Stacking all images that have the same MD5 sum // First make a map of MD5 sums with corresponding images Q_FOREACH(const DB::FileName& fileName, m_list) { DB::MD5 sum = fileName.info()->MD5Sum(); if ( DB::ImageDB::instance()->md5Map()->contains( sum ) ) { if (tostack[sum].isEmpty()) tostack.insert(sum, DB::FileNameList() << fileName); else tostack[sum].append(fileName); } } // Then add images to stack (depending on configuration options) for( QMap::ConstIterator it = tostack.constBegin(); it != tostack.constEnd(); ++it ) { if ( tostack[it.key()].count() > 1 ) { DB::FileNameList stack; for ( int i = 0; i < tostack[it.key()].count(); ++i ) { if ( !DB::ImageDB::instance()->getStackFor( tostack[it.key()][i]).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << tostack[it.key()][i]); else if ( m_autostackSkip->isChecked() ) continue; } showIfStacked.append( tostack[it.key()][i] ); stack.append( tostack[it.key()][i]); } if ( stack.size() > 1 ) { Q_FOREACH( const DB::FileName& a, showIfStacked ) { if ( !DB::ImageDB::instance()->getStackFor(a).isEmpty() ) Q_FOREACH( const DB::FileName& b, DB::ImageDB::instance()->getStackFor(a)) toBeShown.append( b ); else toBeShown.append(a); } DB::ImageDB::instance()->stack(stack); } showIfStacked.clear(); } } } /* * This function searches for images based on file version detection configuration. * Images that are detected to be versions of same file are stacked together. */ void AutoStackImages::matchingFile( DB::FileNameList& toBeShown ) { QMap< DB::MD5, DB::FileNameList > tostack; DB::FileNameList showIfStacked; QString modifiedFileCompString; QRegExp modifiedFileComponent; QStringList originalFileComponents; modifiedFileCompString = Settings::SettingsData::instance()->modifiedFileComponent(); modifiedFileComponent = QRegExp( modifiedFileCompString ); originalFileComponents << Settings::SettingsData::instance()->originalFileComponent(); originalFileComponents = originalFileComponents.at( 0 ).split( QString::fromLatin1(";") ); // Stacking all images based on file version detection // First round prepares the stacking Q_FOREACH( const DB::FileName& fileName, m_list ) { if ( modifiedFileCompString.length() >= 0 && fileName.relative().contains( modifiedFileComponent ) ) { for( QStringList::const_iterator it = originalFileComponents.constBegin(); it != originalFileComponents.constEnd(); ++it ) { QString tmp = fileName.relative(); tmp.replace( modifiedFileComponent, ( *it )); DB::FileName originalFileName = DB::FileName::fromRelativePath( tmp ); if ( originalFileName != fileName && m_list.contains( originalFileName ) ) { DB::MD5 sum = originalFileName.info()->MD5Sum(); if ( tostack[sum].isEmpty() ) { if ( m_origTop->isChecked() ) { tostack.insert( sum, DB::FileNameList() << originalFileName ); tostack[sum].append( fileName ); } else { tostack.insert( sum, DB::FileNameList() << fileName ); tostack[sum].append( originalFileName ); } } else tostack[sum].append(fileName); break; } } } } // Then add images to stack (depending on configuration options) for( QMap::ConstIterator it = tostack.constBegin(); it != tostack.constEnd(); ++it ) { if ( tostack[it.key()].count() > 1 ) { DB::FileNameList stack; for ( int i = 0; i < tostack[it.key()].count(); ++i ) { if ( !DB::ImageDB::instance()->getStackFor( tostack[it.key()][i]).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << tostack[it.key()][i]); else if ( m_autostackSkip->isChecked() ) continue; } showIfStacked.append( tostack[it.key()][i] ); stack.append( tostack[it.key()][i]); } if ( stack.size() > 1 ) { Q_FOREACH( const DB::FileName& a, showIfStacked ) { if ( !DB::ImageDB::instance()->getStackFor(a).isEmpty() ) Q_FOREACH( const DB::FileName& b, DB::ImageDB::instance()->getStackFor(a)) toBeShown.append( b ); else toBeShown.append(a); } DB::ImageDB::instance()->stack(stack); } showIfStacked.clear(); } } } /* * This function searches for images that are shot within specified time frame */ void AutoStackImages::continuousShooting(DB::FileNameList &toBeShown ) { DB::ImageInfoPtr prev; Q_FOREACH(const DB::FileName& fileName, m_list) { DB::ImageInfoPtr info = fileName.info(); // Skipping images that do not have exact time stamp if ( info->date().start() != info->date().end() ) continue; if ( prev && ( prev->date().start().secsTo( info->date().start() ) < m_continuousThreshold->value() ) ) { DB::FileNameList stack; if ( !DB::ImageDB::instance()->getStackFor( prev->fileName() ).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << prev->fileName()); else if ( m_autostackSkip->isChecked() ) continue; } if ( !DB::ImageDB::instance()->getStackFor(fileName).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << fileName); else if ( m_autostackSkip->isChecked() ) continue; } stack.append(prev->fileName()); stack.append(info->fileName()); if ( !toBeShown.isEmpty() ) { if ( toBeShown.at( toBeShown.size() - 1 ).info()->fileName() != prev->fileName() ) toBeShown.append(prev->fileName()); } else { // if this is first insert, we have to include also the stacked images from previuous image if ( !DB::ImageDB::instance()->getStackFor( info->fileName() ).isEmpty() ) Q_FOREACH( const DB::FileName& a, DB::ImageDB::instance()->getStackFor( prev->fileName() ) ) toBeShown.append( a ); else toBeShown.append(prev->fileName()); } // Inserting stacked images from the current image if ( !DB::ImageDB::instance()->getStackFor( info->fileName() ).isEmpty() ) Q_FOREACH( const DB::FileName& a, DB::ImageDB::instance()->getStackFor(fileName)) toBeShown.append( a ); else toBeShown.append(info->fileName()); DB::ImageDB::instance()->stack(stack); } prev = info; } } void AutoStackImages::accept() { QDialog::accept(); Utilities::ShowBusyCursor dummy; DB::FileNameList toBeShown; if ( m_matchingMD5->isChecked() ) matchingMD5( toBeShown ); if ( m_matchingFile->isChecked() ) matchingFile( toBeShown ); if ( m_continuousShooting->isChecked() ) continuousShooting( toBeShown ); MainWindow::Window::theMainWindow()->showThumbNails(toBeShown); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/Util.cpp b/Utilities/Util.cpp index 68fda27e..50461ad9 100644 --- a/Utilities/Util.cpp +++ b/Utilities/Util.cpp @@ -1,682 +1,671 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen 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. 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. 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. */ #include "Util.h" #include "Logging.h" #include #include -#include #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #include #include -#include extern "C" { -#include -#include -#include -#include -#include -#include #include -#include } namespace { // Determined experimentally to yield best results (on Seagate 2TB 2.5" disk, // 5400 RPM). Performance is very similar at 524288. Above that, performance // was significantly worse. Below that, performance also deteriorated. // This assumes use of one image scout thread (see DB/ImageScout.cpp). Without // a scout thread, performance was about 10-15% worse. constexpr int MD5_BUFFER_SIZE = 262144; } /** * Add a line label + info text to the result text if info is not empty. * If the result already contains something, a HTML newline is added first. * To be used in createInfoText(). */ static void AddNonEmptyInfo(const QString &label, const QString &info, QString *result) { if (info.isEmpty()) return; if (!result->isEmpty()) *result += QString::fromLatin1("
"); result->append(label).append(info); } /** * Given an ImageInfoPtr this function will create an HTML blob about the * image. The blob is used in the viewer and in the tool tip box from the * thumbnail view. * * As the HTML text is created, the parameter linkMap is filled with * information about hyperlinks. The map maps from an index to a pair of * (categoryName, categoryItem). This linkMap is used when the user selects * one of the hyberlinks. */ QString Utilities::createInfoText( DB::ImageInfoPtr info, QMap< int,QPair >* linkMap ) { Q_ASSERT( info ); QString result; if ( Settings::SettingsData::instance()->showFilename() ) { AddNonEmptyInfo(i18n("File Name: "), info->fileName().relative(), &result); } if ( Settings::SettingsData::instance()->showDate() ) { AddNonEmptyInfo(i18n("Date: "), info->date().toString( Settings::SettingsData::instance()->showTime() ? true : false ), &result); } /* XXX */ if ( Settings::SettingsData::instance()->showImageSize() && info->mediaType() == DB::Image) { const QSize imageSize = info->size(); // Do not add -1 x -1 text if (imageSize.width() >= 0 && imageSize.height() >= 0) { const double megapix = imageSize.width() * imageSize.height() / 1000000.0; QString info = i18nc("width x height","%1x%2" ,QString::number(imageSize.width()) ,QString::number(imageSize.height())); if (megapix > 0.05) { info += i18nc("short for: x megapixels"," (%1MP)" ,QString::number(megapix, 'f', 1)); } const double aspect = (double) imageSize.width() / (double) imageSize.height(); if (aspect > 1) info += i18nc("aspect ratio"," (%1:1)" ,QLocale::system().toString(aspect, 'f', 2)); else if (aspect >= 0.995 && aspect < 1.005) info += i18nc("aspect ratio"," (1:1)"); else info += i18nc("aspect ratio"," (1:%1)" ,QLocale::system().toString(1.0/aspect, 'f', 2)); AddNonEmptyInfo(i18n("Image Size: "), info, &result); } } if ( Settings::SettingsData::instance()->showRating() ) { if ( info->rating() != -1 ) { if ( ! result.isEmpty() ) result += QString::fromLatin1("
"); QUrl rating; rating.setScheme(QString::fromLatin1("kratingwidget")); // we don't use the host part, but if we don't set it, we can't use port: rating.setHost(QString::fromLatin1("int")); rating.setPort(qMin( qMax( static_cast(0), info->rating() ), static_cast(10))); result += QString::fromLatin1("").arg( rating.toString(QUrl::None) ); } } QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); int link = 0; Q_FOREACH( const DB::CategoryPtr category, categories ) { const QString categoryName = category->name(); if ( category->doShow() ) { StringSet items = info->itemsOfCategory( categoryName ); if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() && ! Settings::SettingsData::instance()->untaggedImagesTagVisible()) { if (categoryName == Settings::SettingsData::instance()->untaggedCategory()) { if (items.contains(Settings::SettingsData::instance()->untaggedTag())) { items.remove(Settings::SettingsData::instance()->untaggedTag()); } } } if (!items.empty()) { QString title = QString::fromUtf8("%1: ").arg(category->name()); QString infoText; bool first = true; Q_FOREACH( const QString &item, items) { if ( first ) first = false; else infoText += QString::fromLatin1( ", " ); if ( linkMap ) { ++link; (*linkMap)[link] = QPair( categoryName, item ); infoText += QString::fromLatin1( "%2").arg( link ).arg( item ); infoText += formatAge(category, item, info); } else infoText += item; } AddNonEmptyInfo(title, infoText, &result); } } } if ( Settings::SettingsData::instance()->showLabel()) { AddNonEmptyInfo(i18n("Label: "), info->label(), &result); } if ( Settings::SettingsData::instance()->showDescription() && !info->description().trimmed().isEmpty() ) { AddNonEmptyInfo(i18n("Description: "), info->description(), &result); } QString exifText; if ( Settings::SettingsData::instance()->showEXIF() ) { typedef QMap ExifMap; typedef ExifMap::const_iterator ExifMapIterator; ExifMap exifMap = Exif::Info::instance()->infoForViewer( info->fileName(), Settings::SettingsData::instance()->iptcCharset() ); for( ExifMapIterator exifIt = exifMap.constBegin(); exifIt != exifMap.constEnd(); ++exifIt ) { if ( exifIt.key().startsWith( QString::fromLatin1( "Exif." ) ) ) for ( QStringList::const_iterator valuesIt = exifIt.value().constBegin(); valuesIt != exifIt.value().constEnd(); ++valuesIt ) { QString exifName = exifIt.key().split( QChar::fromLatin1('.') ).last(); AddNonEmptyInfo(QString::fromLatin1( "%1: ").arg(exifName), *valuesIt, &exifText); } } QString iptcText; for( ExifMapIterator exifIt = exifMap.constBegin(); exifIt != exifMap.constEnd(); ++exifIt ) { if ( !exifIt.key().startsWith( QString::fromLatin1( "Exif." ) ) ) for ( QStringList::const_iterator valuesIt = exifIt.value().constBegin(); valuesIt != exifIt.value().constEnd(); ++valuesIt ) { QString iptcName = exifIt.key().split( QChar::fromLatin1('.') ).last(); AddNonEmptyInfo(QString::fromLatin1( "%1: ").arg(iptcName), *valuesIt, &iptcText); } } if ( !iptcText.isEmpty() ) { if ( exifText.isEmpty() ) exifText = iptcText; else exifText += QString::fromLatin1( "
" ) + iptcText; } } if ( !result.isEmpty() && !exifText.isEmpty() ) result += QString::fromLatin1( "
" ); result += exifText; return result; } using DateSpec = QPair; DateSpec dateDiff(const QDate& birthDate, const QDate& imageDate) { const int bday = birthDate.day(); const int iday = imageDate.day(); const int bmonth = birthDate.month(); const int imonth = imageDate.month(); const int byear = birthDate.year(); const int iyear = imageDate.year(); // Image before birth const int diff = birthDate.daysTo(imageDate); if (diff < 0) return qMakePair(0, 'I'); if (diff < 31) return qMakePair(diff, 'D'); int months = (iyear-byear)*12; months += (imonth-bmonth); months += (iday >= bday) ? 0 : -1; if ( months < 24) return qMakePair(months, 'M'); else return qMakePair(months/12, 'Y'); } QString formatDate(const DateSpec& date) { if (date.second == 'I') return {}; else if (date.second == 'D') return i18np("1 day", "%1 days", date.first); else if (date.second == 'M') return i18np("1 month", "%1 months", date.first); else return i18np("1 year", "%1 years", date.first); } void test() { Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,7,11))) == QString::fromLatin1("0 days")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,8,10))) == QString::fromLatin1("30 days")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,8,11))) == QString::fromLatin1("1 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,8,12))) == QString::fromLatin1("1 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,9,10))) == QString::fromLatin1("1 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,9,11))) == QString::fromLatin1("2 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,6,10))) == QString::fromLatin1("10 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,6,11))) == QString::fromLatin1("11 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,6,12))) == QString::fromLatin1("11 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,7,10))) == QString::fromLatin1("11 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,7,11))) == QString::fromLatin1("12 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,7,12))) == QString::fromLatin1("12 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,12,11))) == QString::fromLatin1("17 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1973,7,11))) == QString::fromLatin1("2 years")); } QString Utilities::formatAge(DB::CategoryPtr category, const QString &item, DB::ImageInfoPtr info) { // test(); // I wish I could get my act together to set up a test suite. const QDate birthDate = category->birthDate(item); const QDate start = info->date().start().date(); const QDate end = info->date().end().date(); if (birthDate.isNull() || start.isNull()) return {}; if ( start == end) return QString::fromUtf8(" (%1)").arg(formatDate(dateDiff(birthDate, start))); else { DateSpec lower = dateDiff(birthDate,start); DateSpec upper = dateDiff(birthDate,end); if (lower == upper) return QString::fromUtf8(" (%1)").arg(formatDate(lower)); else if (lower.second == 'I') return QString::fromUtf8(" (< %1)").arg(formatDate(upper)); else { if (lower.second == upper.second) return QString::fromUtf8(" (%1-%2)").arg(lower.first).arg(formatDate(upper)); else return QString::fromUtf8(" (%1-%2)").arg(formatDate(lower)).arg(formatDate(upper)); } } } void Utilities::checkForBackupFile( const QString& fileName, const QString& message ) { QString backupName = QFileInfo( fileName ).absolutePath() + QString::fromLatin1("/.#") + QFileInfo( fileName ).fileName(); QFileInfo backUpFile( backupName); QFileInfo indexFile( fileName ); if ( !backUpFile.exists() || indexFile.lastModified() > backUpFile.lastModified() || backUpFile.size() == 0 ) if ( !( backUpFile.exists() && !message.isNull() ) ) return; int code; if ( message.isNull() ) code = KMessageBox::questionYesNo( nullptr, i18n("Autosave file '%1' exists (size %3 KB) and is newer than '%2'. " "Should the autosave file be used?", backupName, fileName, backUpFile.size() >> 10 ), i18n("Found Autosave File") ); else if ( backUpFile.size() > 0 ) code = KMessageBox::warningYesNo( nullptr,i18n( "

Error: Cannot use current database file '%1':

%2

" "

Do you want to use autosave (%3 - size %4 KB) instead of exiting?

" "

(Manually verifying and copying the file might be a good idea)

", fileName, message, backupName, backUpFile.size() >> 10 ), i18n("Recover from Autosave?") ); else { KMessageBox::error( nullptr, i18n( "

Error: %1

Also autosave file is empty, check manually " "if numbered backup files exist and can be used to restore index.xml.

", message ) ); exit(-1); } if ( code == KMessageBox::Yes ) { QFile in( backupName ); if ( in.open( QIODevice::ReadOnly ) ) { QFile out( fileName ); if (out.open( QIODevice::WriteOnly ) ) { char data[1024]; int len; while ( (len = in.read( data, 1024 ) ) ) out.write( data, len ); } } } else if ( !message.isNull() ) exit(-1); } bool Utilities::ctrlKeyDown() { return QApplication::keyboardModifiers() & Qt::ControlModifier; } void Utilities::copyList( const QStringList& from, const QString& directoryTo ) { for( QStringList::ConstIterator it = from.constBegin(); it != from.constEnd(); ++it ) { QString destFile = directoryTo + QString::fromLatin1( "/" ) + QFileInfo(*it).fileName(); if ( ! QFileInfo( destFile ).exists() ) { const bool ok = copy( *it, destFile ); if ( !ok ) { KMessageBox::error( nullptr, i18n("Unable to copy '%1' to '%2'.", *it , destFile ), i18n("Error Running Demo") ); exit(-1); } } } } QString Utilities::setupDemo() { QString demoDir = QString::fromLatin1( "%1/kphotoalbum-demo-%2" ).arg(QDir::tempPath()).arg(QString::fromLocal8Bit( qgetenv( "LOGNAME" ) )); QFileInfo fi(demoDir); if ( ! fi.exists() ) { bool ok = QDir().mkdir( demoDir ); if ( !ok ) { KMessageBox::error( nullptr, i18n("Unable to create directory '%1' needed for demo.", demoDir ), i18n("Error Running Demo") ); exit(-1); } } // index.xml QString demoDB = locateDataFile(QString::fromLatin1("demo/index.xml")); if ( demoDB.isEmpty() ) { qCDebug(UtilitiesLog) << "No demo database in standard locations:" << QStandardPaths::standardLocations(QStandardPaths::DataLocation); exit(-1); } QString configFile = demoDir + QString::fromLatin1( "/index.xml" ); copy(demoDB, configFile); // Images const QStringList kpaDemoDirs = QStandardPaths::locateAll( QStandardPaths::DataLocation, QString::fromLatin1("demo"), QStandardPaths::LocateDirectory); QStringList images; Q_FOREACH(const QString &dir, kpaDemoDirs) { QDirIterator it(dir, QStringList() << QStringLiteral("*.jpg") << QStringLiteral("*.avi")); while (it.hasNext()) { images.append(it.next()); } } copyList( images, demoDir ); // CategoryImages QString catDir = demoDir + QString::fromLatin1("/CategoryImages"); fi = QFileInfo(catDir); if ( ! fi.exists() ) { bool ok = QDir().mkdir( catDir ); if ( !ok ) { KMessageBox::error( nullptr, i18n("Unable to create directory '%1' needed for demo.", catDir ), i18n("Error Running Demo") ); exit(-1); } } const QStringList kpaDemoCatDirs = QStandardPaths::locateAll( QStandardPaths::DataLocation, QString::fromLatin1("demo/CategoryImages"), QStandardPaths::LocateDirectory); QStringList catImages; Q_FOREACH(const QString &dir, kpaDemoCatDirs) { QDirIterator it(dir, QStringList() << QStringLiteral("*.jpg")); while (it.hasNext()) { catImages.append(it.next()); } } copyList( catImages, catDir ); return configFile; } bool Utilities::copy( const QString& from, const QString& to ) { if ( QFileInfo(to).exists()) QDir().remove(to); return QFile::copy(from,to); } bool Utilities::makeHardLink( const QString& from, const QString& to ) { if (link(from.toLocal8Bit().constData(), to.toLocal8Bit().constData()) != 0) return false; else return true; } bool Utilities::makeSymbolicLink( const QString& from, const QString& to ) { if (symlink(from.toLocal8Bit().constData(), to.toLocal8Bit().constData()) != 0) return false; else return true; } bool Utilities::canReadImage( const DB::FileName& fileName ) { bool fastMode = !Settings::SettingsData::instance()->ignoreFileExtension(); QMimeDatabase::MatchMode mode = fastMode ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault; QMimeDatabase db; QMimeType mimeType = db.mimeTypeForFile( fileName.absolute(), mode ); return QImageReader::supportedMimeTypes().contains( mimeType.name().toUtf8() ) || ImageManager::ImageDecoder::mightDecode( fileName ); } QString Utilities::locateDataFile(const QString& fileName) { return QStandardPaths::locate(QStandardPaths::DataLocation, fileName); } QString Utilities::readFile( const QString& fileName ) { if ( fileName.isEmpty() ) { KMessageBox::error( nullptr, i18n("

No file name given!

") ); return QString(); } QFile file( fileName ); if ( !file.open( QIODevice::ReadOnly ) ) { //KMessageBox::error( nullptr, i18n("Could not open file %1").arg( fileName ) ); return QString(); } QTextStream stream( &file ); QString content = stream.readAll(); file.close(); return content; } namespace Utilities { QString normalizedFileName( const QString& fileName ) { return QFileInfo(fileName).absoluteFilePath(); } QString dereferenceSymLinks( const QString& fileName ) { QFileInfo fi(fileName); int rounds = 256; while (fi.isSymLink() && --rounds > 0) fi = QFileInfo(fi.readLink()); if (rounds == 0) return QString(); return fi.filePath(); } } QString Utilities::stripEndingForwardSlash( const QString& fileName ) { static QString slash = QString::fromLatin1("/"); if ( fileName.endsWith( slash ) ) return fileName.left( fileName.length()-1); else return fileName; } QString Utilities::relativeFolderName( const QString& fileName) { int index= fileName.lastIndexOf( QChar::fromLatin1('/'), -1); if (index == -1) return QString(); else return fileName.left( index ); } void Utilities::deleteDemo() { QString dir = QString::fromLatin1( "%1/kphotoalbum-demo-%2" ).arg(QDir::tempPath()).arg(QString::fromLocal8Bit( qgetenv( "LOGNAME" ) ) ); QUrl demoUrl = QUrl::fromLocalFile( dir ); KJob *delDemoJob = KIO::del( demoUrl ); KJobWidgets::setWindow( delDemoJob, MainWindow::Window::theMainWindow()); delDemoJob->exec(); } QString Utilities::absoluteImageFileName( const QString& relativeName ) { return stripEndingForwardSlash( Settings::SettingsData::instance()->imageDirectory() ) + QString::fromLatin1( "/" ) + relativeName; } QString Utilities::imageFileNameToAbsolute( const QString& fileName ) { if ( fileName.startsWith( Settings::SettingsData::instance()->imageDirectory() ) ) return fileName; else if ( fileName.startsWith( QString::fromLatin1("file://") ) ) return imageFileNameToAbsolute( fileName.mid( 7 ) ); // 7 == length("file://") else if ( fileName.startsWith( QString::fromLatin1("/") ) ) return QString(); // Not within our image root else return absoluteImageFileName( fileName ); } bool operator>( const QPoint& p1, const QPoint& p2) { return p1.y() > p2.y() || (p1.y() == p2.y() && p1.x() > p2.x() ); } bool operator<( const QPoint& p1, const QPoint& p2) { return p1.y() < p2.y() || ( p1.y() == p2.y() && p1.x() < p2.x() ); } const QSet& Utilities::supportedVideoExtensions() { static QSet videoExtensions; if ( videoExtensions.empty() ) { videoExtensions.insert( QString::fromLatin1( "3gp" ) ); videoExtensions.insert( QString::fromLatin1( "avi" ) ); videoExtensions.insert( QString::fromLatin1( "mp4" ) ); videoExtensions.insert( QString::fromLatin1( "m4v" ) ); videoExtensions.insert( QString::fromLatin1( "mpeg" ) ); videoExtensions.insert( QString::fromLatin1( "mpg" ) ); videoExtensions.insert( QString::fromLatin1( "qt" ) ); videoExtensions.insert( QString::fromLatin1( "mov" ) ); videoExtensions.insert( QString::fromLatin1( "moov" ) ); videoExtensions.insert( QString::fromLatin1( "qtvr" ) ); videoExtensions.insert( QString::fromLatin1( "rv" ) ); videoExtensions.insert( QString::fromLatin1( "3g2" ) ); videoExtensions.insert( QString::fromLatin1( "fli" ) ); videoExtensions.insert( QString::fromLatin1( "flc" ) ); videoExtensions.insert( QString::fromLatin1( "mkv" ) ); videoExtensions.insert( QString::fromLatin1( "mng" ) ); videoExtensions.insert( QString::fromLatin1( "asf" ) ); videoExtensions.insert( QString::fromLatin1( "asx" ) ); videoExtensions.insert( QString::fromLatin1( "wmp" ) ); videoExtensions.insert( QString::fromLatin1( "wmv" ) ); videoExtensions.insert( QString::fromLatin1( "ogm" ) ); videoExtensions.insert( QString::fromLatin1( "rm" ) ); videoExtensions.insert( QString::fromLatin1( "flv" ) ); videoExtensions.insert( QString::fromLatin1( "webm" ) ); videoExtensions.insert( QString::fromLatin1( "mts" ) ); videoExtensions.insert( QString::fromLatin1( "ogg" ) ); videoExtensions.insert( QString::fromLatin1( "ogv" ) ); videoExtensions.insert( QString::fromLatin1( "m2ts" ) ); } return videoExtensions; } bool Utilities::isVideo( const DB::FileName& fileName ) { QFileInfo fi( fileName.relative() ); QString ext = fi.suffix().toLower(); return supportedVideoExtensions().contains( ext ); } bool Utilities::isRAW( const DB::FileName& fileName ) { return ImageManager::RAWImageDecoder::isRAW( fileName ); } QImage Utilities::scaleImage(const QImage &image, int w, int h, Qt::AspectRatioMode mode ) { return image.scaled( w, h, mode, Settings::SettingsData::instance()->smoothScale() ? Qt::SmoothTransformation : Qt::FastTransformation ); } QImage Utilities::scaleImage(const QImage &image, const QSize& s, Qt::AspectRatioMode mode ) { return scaleImage( image, s.width(), s.height(), mode ); } QString Utilities::cStringWithEncoding( const char *c_str, const QString& charset ) { QTextCodec* codec = QTextCodec::codecForName( charset.toLatin1() ); if (!codec) codec = QTextCodec::codecForLocale(); return codec->toUnicode( c_str ); } DB::MD5 Utilities::MD5Sum( const DB::FileName& fileName ) { DB::MD5 checksum; QFile file( fileName.absolute() ); if ( file.open( QIODevice::ReadOnly ) ) { QCryptographicHash md5calculator(QCryptographicHash::Md5); while ( !file.atEnd() ) { QByteArray md5Buffer( file.read( MD5_BUFFER_SIZE ) ); md5calculator.addData( md5Buffer ); } file.close(); checksum = DB::MD5(QString::fromLatin1(md5calculator.result().toHex())); } return checksum; } QColor Utilities::contrastColor( const QColor& col ) { if ( col.red() < 127 && col.green() < 127 && col.blue() < 127 ) return Qt::white; else return Qt::black; } void Utilities::saveImage( const DB::FileName& fileName, const QImage& image, const char* format ) { const QFileInfo info(fileName.absolute()); QDir().mkpath(info.path()); const bool ok = image.save(fileName.absolute(),format); Q_ASSERT(ok); Q_UNUSED(ok); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/Util.h b/Utilities/Util.h index f1f54967..e961a08d 100644 --- a/Utilities/Util.h +++ b/Utilities/Util.h @@ -1,79 +1,75 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen 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. 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. 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. */ #ifndef UTIL_H #define UTIL_H -#include -#include -#include -#include -#include -#include "Settings/SettingsData.h" -#include "DB/ImageInfoList.h" -#include +#include "DB/CategoryPtr.h" +#include "DB/FileName.h" +#include "DB/ImageInfoPtr.h" #include "DB/MD5.h" -namespace DB -{ -class ImageInfo; -} +#include +#include +#include +#include +#include namespace Utilities { QString createInfoText( DB::ImageInfoPtr info, QMap >* ); QString formatAge(DB::CategoryPtr category,const QString& item, DB::ImageInfoPtr info); void checkForBackupFile( const QString& fileName, const QString& message = QString() ); bool ctrlKeyDown(); bool copy( const QString& from, const QString& to ); void copyList( const QStringList& from, const QString& directoryTo ); bool makeSymbolicLink( const QString& from, const QString& to ); bool makeHardLink( const QString& from, const QString& to ); void deleteDemo(); QString setupDemo(); bool canReadImage( const DB::FileName& fileName ); const QSet& supportedVideoExtensions(); bool isVideo( const DB::FileName& fileName ); bool isRAW( const DB::FileName& fileName ); QString locateDataFile(const QString& fileName); QString readFile( const QString& fileName ); QString stripEndingForwardSlash( const QString& fileName ); QString absoluteImageFileName( const QString& relativeName ); QString imageFileNameToAbsolute( const QString& fileName ); QString relativeFolderName( const QString& fileName); QImage scaleImage(const QImage &image, int w, int h, Qt::AspectRatioMode mode=Qt::IgnoreAspectRatio ); QImage scaleImage(const QImage &image, const QSize& s, Qt::AspectRatioMode mode=Qt::IgnoreAspectRatio ); QString cStringWithEncoding( const char *c_str, const QString& charset ); DB::MD5 MD5Sum( const DB::FileName& fileName ); QColor contrastColor( const QColor& ); void saveImage( const DB::FileName& fileName, const QImage& image, const char* format ); } bool operator>( const QPoint&, const QPoint& ); bool operator<( const QPoint&, const QPoint& ); #endif /* UTIL_H */ // vi:expandtab:tabstop=4 shiftwidth=4: