diff --git a/DB/ImageInfo.cpp b/DB/ImageInfo.cpp index 123ade0d..5a108027 100644 --- a/DB/ImageInfo.cpp +++ b/DB/ImageInfo.cpp @@ -1,864 +1,802 @@ /* Copyright (C) 2003-2015 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 "ImageInfo.h" #include "FileInfo.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include #include using namespace DB; ImageInfo::ImageInfo() :m_null( true ), m_rating(-1), m_stackId(0), m_stackOrder(0) , m_videoLength(-1) , m_isMatched( false ), m_matchGeneration( -1 ) , m_locked( false ), m_dirty( false ), m_delaySaving( false ) { } ImageInfo::ImageInfo( const DB::FileName& fileName, MediaType type, bool readExifInfo, bool storeExifInfo) : m_imageOnDisk( YesOnDisk ), m_null( false ), m_size( -1, -1 ), m_type( type ) , m_rating(-1), m_stackId(0), m_stackOrder(0) , m_videoLength(-1) , m_isMatched( false ), m_matchGeneration( -1 ) , m_locked(false), m_delaySaving( true ) { QFileInfo fi( fileName.absolute() ); m_label = fi.completeBaseName(); m_angle = 0; setFileName(fileName); // Read Exif information if ( readExifInfo ) { ExifMode mode = EXIFMODE_INIT; if ( ! storeExifInfo) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif(fileName, mode); } m_dirty = false; m_delaySaving = false; } void ImageInfo::setIsMatched(bool isMatched) { m_isMatched = isMatched; } bool ImageInfo::isMatched() const { return m_isMatched; } void ImageInfo::setMatchGeneration(int matchGeneration) { m_matchGeneration = matchGeneration; } int ImageInfo::matchGeneration() const { return m_matchGeneration; } /** Change delaying of saving changes. * * Will save changes when set to false. * * Use this method to set multiple attributes with only one * database operation. * * Example: * \code * info.delaySavingChanges(true); * info.setLabel("Hello"); * info.setDescription("Hello world"); * info.delaySavingChanges(false); * \endcode * * \see saveChanges() */ void ImageInfo::delaySavingChanges(bool b) { m_delaySaving = b; if (!b) saveChanges(); } void ImageInfo::setLabel( const QString& desc ) { if (desc != m_label) m_dirty = true; m_label = desc; saveChangesIfNotDelayed(); } QString ImageInfo::label() const { return m_label; } void ImageInfo::setDescription( const QString& desc ) { if (desc != m_description) m_dirty = true; m_description = desc.trimmed(); saveChangesIfNotDelayed(); } QString ImageInfo::description() const { return m_description; } void ImageInfo::setCategoryInfo( const QString& key, const StringSet& value ) { // Don't check if really changed, because it's too slow. m_dirty = true; m_categoryInfomation[key] = value; saveChangesIfNotDelayed(); } bool ImageInfo::hasCategoryInfo( const QString& key, const QString& value ) const { return m_categoryInfomation[key].contains(value); } bool DB::ImageInfo::hasCategoryInfo( const QString& key, const StringSet& values ) const { return values.intersects( m_categoryInfomation[key] ); } StringSet ImageInfo::itemsOfCategory( const QString& key ) const { return m_categoryInfomation[key]; } void ImageInfo::renameItem( const QString& category, const QString& oldValue, const QString& newValue ) { if (m_taggedAreas.contains(category)) { if (m_taggedAreas[category].contains(oldValue)) { m_taggedAreas[category][newValue] = m_taggedAreas[category][oldValue]; m_taggedAreas[category].remove(oldValue); } } StringSet& set = m_categoryInfomation[category]; StringSet::iterator it = set.find( oldValue ); if ( it != set.end() ) { m_dirty = true; set.erase( it ); set.insert( newValue ); saveChangesIfNotDelayed(); } } DB::FileName ImageInfo::fileName() const { return m_fileName; } void ImageInfo::setFileName( const DB::FileName& fileName ) { if (fileName != m_fileName) m_dirty = true; m_fileName = fileName; m_imageOnDisk = Unchecked; DB::CategoryPtr folderCategory = DB::ImageDB::instance()->categoryCollection()-> categoryForSpecial(DB::Category::FolderCategory); if (folderCategory) { DB::MemberMap& map = DB::ImageDB::instance()->memberMap(); createFolderCategoryItem( folderCategory, map ); //ImageDB::instance()->setMemberMap( map ); } saveChangesIfNotDelayed(); } void ImageInfo::rotate( int degrees, RotationMode mode ) { // ensure positive degrees: degrees += 360; degrees = degrees % 360; if ( degrees == 0 ) return; m_dirty = true; m_angle = ( m_angle + degrees ) % 360; if (degrees == 90 || degrees == 270) { m_size.transpose(); } // the AnnotationDialog manages this by itself and sets RotateImageInfoOnly: if ( mode == RotateImageInfoAndAreas ) { for ( auto& areasOfCategory : m_taggedAreas ) { for ( auto& area : areasOfCategory ) { QRect rotatedArea; // parameter order for QRect::setCoords: // setCoords( left, top, right, bottom ) // keep in mind that _size is already transposed switch (degrees) { case 90: rotatedArea.setCoords( m_size.width() - area.bottom(), area.left(), m_size.width() - area.top(), area.right() ); break; case 180: rotatedArea.setCoords( m_size.width() - area.right(), m_size.height() - area.bottom(), m_size.width() - area.left(), m_size.height() - area.top() ); break; case 270: rotatedArea.setCoords( area.top(), m_size.height() - area.right(), area.bottom(), m_size.height() - area.left() ); break; default: // degrees==0; "odd" values won't happen. rotatedArea = area; break; } // update _taggedAreas[category][tag]: area = rotatedArea; } } } saveChangesIfNotDelayed(); } int ImageInfo::angle() const { return m_angle; } void ImageInfo::setAngle( int angle ) { if (angle != m_angle) m_dirty = true; m_angle = angle; saveChangesIfNotDelayed(); } short ImageInfo::rating() const { return m_rating; } void ImageInfo::setRating( short rating ) { Q_ASSERT( (rating >= 0 && rating <= 10) || rating == -1 ); if ( rating > 10 ) rating = 10; if ( rating < -1 ) rating = -1; if ( m_rating != rating ) m_dirty = true; m_rating = rating; saveChangesIfNotDelayed(); } DB::StackID ImageInfo::stackId() const { return m_stackId; } void ImageInfo::setStackId( const DB::StackID stackId ) { if ( stackId != m_stackId ) m_dirty = true; m_stackId = stackId; saveChangesIfNotDelayed(); } unsigned int ImageInfo::stackOrder() const { return m_stackOrder; } void ImageInfo::setStackOrder( const unsigned int stackOrder ) { if ( stackOrder != m_stackOrder ) m_dirty = true; m_stackOrder = stackOrder; saveChangesIfNotDelayed(); } void ImageInfo::setVideoLength(int length) { if ( m_videoLength != length ) m_dirty = true; m_videoLength = length; saveChangesIfNotDelayed(); } int ImageInfo::videoLength() const { return m_videoLength; } void ImageInfo::setDate( const ImageDate& date ) { if (date != m_date) m_dirty = true; m_date = date; saveChangesIfNotDelayed(); } ImageDate& ImageInfo::date() { return m_date; } ImageDate ImageInfo::date() const { return m_date; } bool ImageInfo::operator!=( const ImageInfo& other ) const { return !(*this == other); } bool ImageInfo::operator==( const ImageInfo& other ) const { bool changed = ( m_fileName != other.m_fileName || m_label != other.m_label || ( !m_description.isEmpty() && !other.m_description.isEmpty() && m_description != other.m_description ) || // one might be isNull. m_date != other.m_date || m_angle != other.m_angle || m_rating != other.m_rating || ( m_stackId != other.m_stackId || ! ( ( m_stackId == 0 ) ? true : ( m_stackOrder == other.m_stackOrder ) ) ) ); if ( !changed ) { QStringList keys = DB::ImageDB::instance()->categoryCollection()->categoryNames(); for( QStringList::ConstIterator it = keys.constBegin(); it != keys.constEnd(); ++it ) changed |= m_categoryInfomation[*it] != other.m_categoryInfomation[*it]; } return !changed; } void ImageInfo::renameCategory( const QString& oldName, const QString& newName ) { m_dirty = true; m_categoryInfomation[newName] = m_categoryInfomation[oldName]; m_categoryInfomation.remove(oldName); m_taggedAreas[newName] = m_taggedAreas[oldName]; m_taggedAreas.remove(oldName); saveChangesIfNotDelayed(); } void ImageInfo::setMD5Sum( const MD5& sum, bool storeEXIF ) { if (sum != m_md5sum) { // if we make a QObject derived class out of imageinfo, we might invalidate thumbnails from here // file changed -> reload/invalidate metadata: ExifMode mode = EXIFMODE_ORIENTATION | EXIFMODE_DATABASE_UPDATE; // fuzzy dates are usually set for a reason if (!m_date.isFuzzy()) mode |= EXIFMODE_DATE; // FIXME (ZaJ): the "right" thing to do would be to update the description // - if it is currently empty (done.) // - if it has been set from the exif info and not been changed (TODO) if (m_description.isEmpty()) mode |= EXIFMODE_DESCRIPTION; if (!storeEXIF) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif( fileName(), mode); // FIXME (ZaJ): it *should* make sense to set the ImageDB::md5Map() from here, but I want // to make sure I fully understand everything first... // this could also be done as signal md5Changed(old,new) // image size is invalidated by the thumbnail builder, if needed m_dirty = true; } m_md5sum = sum; saveChangesIfNotDelayed(); } void ImageInfo::setLocked( bool locked ) { m_locked = locked; } bool ImageInfo::isLocked() const { return m_locked; } void ImageInfo::readExif(const DB::FileName& fullPath, DB::ExifMode mode) { DB::FileInfo exifInfo = DB::FileInfo::read( fullPath, mode ); bool oldDelaySaving = m_delaySaving; delaySavingChanges(true); // Date if ( updateDateInformation(mode) ) { const ImageDate newDate ( exifInfo.dateTime() ); setDate( newDate ); } // Orientation if ( (mode & EXIFMODE_ORIENTATION) && Settings::SettingsData::instance()->useEXIFRotate() ) { setAngle( exifInfo.angle() ); } // Description if ( (mode & EXIFMODE_DESCRIPTION) && Settings::SettingsData::instance()->useEXIFComments() ) { bool doSetDescription = true; QString desc = exifInfo.description(); if ( Settings::SettingsData::instance()->stripEXIFComments() ) { for( const auto& ignoredComment : Settings::SettingsData::instance()->EXIFCommentsToStrip() ) { if ( desc == ignoredComment ) { doSetDescription = false; break; } } } if (doSetDescription) { setDescription(desc); } } delaySavingChanges(false); m_delaySaving = oldDelaySaving; // Database update if ( mode & EXIFMODE_DATABASE_UPDATE ) { Exif::Database::instance()->add( exifInfo ); #ifdef HAVE_KGEOMAP // GPS coords might have changed... m_coordsIsSet = false; #endif } } QStringList ImageInfo::availableCategories() const { return m_categoryInfomation.keys(); } QSize ImageInfo::size() const { return m_size; } void ImageInfo::setSize( const QSize& size ) { if (size != m_size) m_dirty = true; m_size = size; saveChangesIfNotDelayed(); } bool ImageInfo::imageOnDisk( const DB::FileName& fileName ) { return fileName.exists(); } ImageInfo::ImageInfo( const DB::FileName& fileName, const QString& label, const QString& description, const ImageDate& date, int angle, const MD5& md5sum, const QSize& size, MediaType type, short rating, unsigned int stackId, unsigned int stackOrder ) { m_delaySaving = true; m_fileName = fileName; m_label =label; m_description =description; m_date = date; m_angle =angle; m_md5sum =md5sum; m_size = size; m_imageOnDisk = Unchecked; m_locked = false; m_null = false; m_type = type; m_dirty = true; delaySavingChanges(false); if ( rating > 10 ) rating = 10; if ( rating < -1 ) rating = -1; m_rating = rating; m_stackId = stackId; m_stackOrder = stackOrder; m_videoLength= -1; } // TODO: we should get rid of this operator. It seems only be necessary // because of the 'delaySavings' field that gets a special value. // ImageInfo should just be a dumb data object holder and not incorporate // storing strategies. ImageInfo& ImageInfo::operator=( const ImageInfo& other ) { m_fileName = other.m_fileName; m_label = other.m_label; m_description = other.m_description; m_date = other.m_date; m_categoryInfomation = other.m_categoryInfomation; m_taggedAreas = other.m_taggedAreas; m_angle = other.m_angle; m_imageOnDisk = other.m_imageOnDisk; m_md5sum = other.m_md5sum; m_null = other.m_null; m_size = other.m_size; m_type = other.m_type; m_dirty = other.m_dirty; m_rating = other.m_rating; m_stackId = other.m_stackId; m_stackOrder = other.m_stackOrder; m_videoLength = other.m_videoLength; delaySavingChanges(false); return *this; } MediaType DB::ImageInfo::mediaType() const { return m_type; } bool ImageInfo::isVideo() const { return m_type == Video; } void DB::ImageInfo::createFolderCategoryItem( DB::CategoryPtr folderCategory, DB::MemberMap& memberMap ) { QString folderName = Utilities::relativeFolderName( m_fileName.relative() ); if ( folderName.isEmpty() ) return; if ( ! memberMap.contains( folderCategory->name(), folderName ) ) { QStringList directories = folderName.split(QString::fromLatin1( "/" ) ); QString curPath; for( QStringList::ConstIterator directoryIt = directories.constBegin(); directoryIt != directories.constEnd(); ++directoryIt ) { if ( curPath.isEmpty() ) curPath = *directoryIt; else { QString oldPath = curPath; curPath = curPath + QString::fromLatin1( "/" ) + *directoryIt; memberMap.addMemberToGroup( folderCategory->name(), oldPath, curPath ); } } folderCategory->addItem( folderName ); } m_categoryInfomation.insert( folderCategory->name() , StringSet() << folderName ); } void DB::ImageInfo::copyExtraData( const DB::ImageInfo& from, bool copyAngle) { m_categoryInfomation = from.m_categoryInfomation; m_description = from.m_description; // Hmm... what should the date be? orig or modified? // _date = from._date; if (copyAngle) m_angle = from.m_angle; m_rating = from.m_rating; } void DB::ImageInfo::removeExtraData () { m_categoryInfomation.clear(); m_description.clear(); m_rating = -1; } -void ImageInfo::merge(const ImageInfo &other) -{ - // Merge date - if ( other.date() != m_date) - { - // a fuzzy date has been set by the user and therefore "wins" over an exact date. - // two fuzzy dates can be merged - // two exact dates should ideally be cross-checked with Exif information in the file. - // Nevertheless, we merge them into a fuzzy date to avoid the complexity of checking the file. - if (other.date().isFuzzy()) - { - if (m_date.isFuzzy()) - m_date.extendTo(other.date()); - else - m_date = other.date(); - } - else if (!m_date.isFuzzy()) - { - m_date.extendTo(other.date()); - } - // else: keep m_date - } - - // Merge description - if ( !other.description().isEmpty() ) { - if ( m_description.isEmpty() ) - m_description = other.description(); - else if (m_description != other.description()) - m_description += QString::fromUtf8("\n-----------\n") + other.m_description; - } - - // Clear untagged tag if only one of the images was untagged - const QString untaggedCategory = Settings::SettingsData::instance()->untaggedCategory(); - const QString untaggedTag = Settings::SettingsData::instance()->untaggedTag(); - const bool isCompleted = !m_categoryInfomation[untaggedCategory].contains(untaggedTag) || !other.m_categoryInfomation[untaggedCategory].contains(untaggedTag); - - // Merge tags - QSet keys = QSet::fromList(m_categoryInfomation.keys()); - keys.unite(QSet::fromList(other.m_categoryInfomation.keys())); - for( const QString& key : keys) { - m_categoryInfomation[key].unite(other.m_categoryInfomation[key]); - } - - // Clear untagged tag if only one of the images was untagged - if (isCompleted) - m_categoryInfomation[untaggedCategory].remove(untaggedTag); - - // merge stacks: - if (isStacked() || other.isStacked()) - { - DB::FileNameList stackImages; - if (!isStacked()) - stackImages.append(fileName()); - else - stackImages.append(DB::ImageDB::instance()->getStackFor(fileName())); - stackImages.append(DB::ImageDB::instance()->getStackFor(other.fileName())); - - DB::ImageDB::instance()->unstack(stackImages); - if (!DB::ImageDB::instance()->stack(stackImages)) - qCWarning(DBLog, "Could not merge stacks!"); - } -} void DB::ImageInfo::addCategoryInfo( const QString& category, const StringSet& values ) { for ( StringSet::const_iterator valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt ) { if (! m_categoryInfomation[category].contains( *valueIt ) ) { m_dirty = true; m_categoryInfomation[category].insert( *valueIt ); } } saveChangesIfNotDelayed(); } void DB::ImageInfo::clearAllCategoryInfo() { m_categoryInfomation.clear(); m_taggedAreas.clear(); } void DB::ImageInfo::removeCategoryInfo( const QString& category, const StringSet& values ) { for ( StringSet::const_iterator valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt ) { if ( m_categoryInfomation[category].contains( *valueIt ) ) { m_dirty = true; m_categoryInfomation[category].remove(*valueIt); m_taggedAreas[category].remove(*valueIt); } } saveChangesIfNotDelayed(); } void DB::ImageInfo::addCategoryInfo( const QString& category, const QString& value, const QRect& area ) { if (! m_categoryInfomation[category].contains( value ) ) { m_dirty = true; m_categoryInfomation[category].insert( value ); if (area.isValid()) { m_taggedAreas[category][value] = area; } } saveChangesIfNotDelayed(); } void DB::ImageInfo::removeCategoryInfo( const QString& category, const QString& value ) { if ( m_categoryInfomation[category].contains( value ) ) { m_dirty = true; m_categoryInfomation[category].remove( value ); m_taggedAreas[category].remove( value ); } saveChangesIfNotDelayed(); } void DB::ImageInfo::setPositionedTags(const QString& category, const QMap &positionedTags) { m_dirty = true; m_taggedAreas[category] = positionedTags; saveChangesIfNotDelayed(); } bool DB::ImageInfo::updateDateInformation( int mode ) const { if ((mode & EXIFMODE_DATE) == 0) return false; if ( (mode & EXIFMODE_FORCE) != 0 ) return true; return true; } QMap> DB::ImageInfo::taggedAreas() const { return m_taggedAreas; } QRect DB::ImageInfo::areaForTag(QString category, QString tag) const { // QMap::value returns a default constructed value if the key is not found: return m_taggedAreas.value(category).value(tag); } #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates DB::ImageInfo::coordinates() const { if (m_coordsIsSet) { return m_coordinates; } static const int EXIF_GPS_VERSIONID = 0; static const int EXIF_GPS_LATREF = 1; static const int EXIF_GPS_LAT = 2; static const int EXIF_GPS_LONREF = 3; static const int EXIF_GPS_LON = 4; static const int EXIF_GPS_ALTREF = 5; static const int EXIF_GPS_ALT = 6; static const QString S = QString::fromUtf8("S"); static const QString W = QString::fromUtf8("W"); static QList fields; if (fields.isEmpty()) { // the order here matters! we use the named int constants afterwards to refer to them: fields.append( new Exif::IntExifElement( "Exif.GPSInfo.GPSVersionID" ) ); // actually a byte value fields.append( new Exif::StringExifElement( "Exif.GPSInfo.GPSLatitudeRef" ) ); fields.append( new Exif::RationalExifElement( "Exif.GPSInfo.GPSLatitude" ) ); fields.append( new Exif::StringExifElement( "Exif.GPSInfo.GPSLongitudeRef" ) ); fields.append( new Exif::RationalExifElement( "Exif.GPSInfo.GPSLongitude" ) ); fields.append( new Exif::IntExifElement( "Exif.GPSInfo.GPSAltitudeRef" ) ); // actually a byte value fields.append( new Exif::RationalExifElement( "Exif.GPSInfo.GPSAltitude" ) ); } // read field values from database: bool foundIt = Exif::Database::instance()->readFields( m_fileName, fields ); // if the Database query result doesn't contain exif GPS info (-> upgraded exifdb from DBVersion < 2), it is null // if the result is int 0, then there's no exif gps information in the image // otherwise we can proceed to parse the information if ( foundIt && fields[EXIF_GPS_VERSIONID]->value().isNull() ) { // update exif DB and repeat the search: Exif::Database::instance()->remove( fileName() ); Exif::Database::instance()->add( fileName() ); Exif::Database::instance()->readFields( m_fileName, fields ); Q_ASSERT( !fields[EXIF_GPS_VERSIONID]->value().isNull() ); } KGeoMap::GeoCoordinates coords; // gps info set? // don't use the versionid field here, because some cameras use 0 as its value if ( foundIt && fields[EXIF_GPS_LAT]->value().toInt() != -1.0 && fields[EXIF_GPS_LON]->value().toInt() != -1.0 ) { // lat/lon/alt reference determines sign of float: double latr = (fields[EXIF_GPS_LATREF]->value().toString() == S ) ? -1.0 : 1.0; double lat = fields[EXIF_GPS_LAT]->value().toFloat(); double lonr = (fields[EXIF_GPS_LONREF]->value().toString() == W ) ? -1.0 : 1.0; double lon = fields[EXIF_GPS_LON]->value().toFloat(); double altr = (fields[EXIF_GPS_ALTREF]->value().toInt() == 1 ) ? -1.0 : 1.0; double alt = fields[EXIF_GPS_ALT]->value().toFloat(); if (lat != -1.0 && lon != -1.0) { coords.setLatLon(latr * lat, lonr * lon); if (alt != 0.0f) { coords.setAlt(altr * alt); } } } m_coordinates = coords; m_coordsIsSet = true; return m_coordinates; } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageInfo.h b/DB/ImageInfo.h index 0c607208..6e89cf55 100644 --- a/DB/ImageInfo.h +++ b/DB/ImageInfo.h @@ -1,248 +1,243 @@ /* Copyright (C) 2003-2015 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 IMAGEINFO_H #define IMAGEINFO_H #include #include #include #include "ImageDate.h" #include "Utilities/StringSet.h" #include "MD5.h" #include "ExifMode.h" #include "DB/CategoryPtr.h" #include #include #include "FileName.h" #include "config-kpa-kgeomap.h" #ifdef HAVE_KGEOMAP #include #endif namespace Plugins { class ImageInfo; } namespace XMLDB { class Database; } namespace DB { enum PathType { RelativeToImageRoot, AbsolutePath }; enum RotationMode { RotateImageInfoAndAreas, RotateImageInfoOnly }; using Utilities::StringSet; class MemberMap; enum MediaType { Image = 0x01, Video = 0x02 }; const MediaType anyMediaType = MediaType(Image | Video); typedef unsigned int StackID; class ImageInfo :public QSharedData { public: ImageInfo(); explicit ImageInfo( const DB::FileName& fileName, MediaType type = Image, bool readExifInfo = true, bool storeExifInfo = true); ImageInfo( const DB::FileName& fileName, const QString& label, const QString& description, const ImageDate& date, int angle, const MD5& md5sum, const QSize& size, MediaType type, short rating = -1, StackID stackId = 0, unsigned int stackOrder = 0 ); virtual ~ImageInfo() { saveChanges(); } FileName fileName() const; void setFileName( const DB::FileName& relativeFileName ); void setLabel( const QString& ); QString label() const; void setDescription( const QString& ); QString description() const; void setDate( const ImageDate& ); ImageDate date() const; ImageDate& date(); void readExif(const DB::FileName& fullPath, DB::ExifMode mode); void rotate( int degrees, RotationMode mode=RotateImageInfoAndAreas ); int angle() const; void setAngle( int angle ); short rating() const; void setRating( short rating ); bool isStacked() const { return m_stackId != 0; } StackID stackId() const; unsigned int stackOrder() const; void setStackOrder( const unsigned int stackOrder ); void setVideoLength(int seconds); int videoLength() const; void setCategoryInfo( const QString& key, const StringSet& value ); void addCategoryInfo( const QString& category, const StringSet& values ); /** * Enable a tag within a category for this image. * Optionally, the tag's position can be given (for positionable categories). * @param category the category name * @param value the tag name * @param area the image region that the tag applies to. */ void addCategoryInfo(const QString& category, const QString& value, const QRect& area = QRect()); void clearAllCategoryInfo(); void removeCategoryInfo( const QString& category, const StringSet& values ); void removeCategoryInfo( const QString& category, const QString& value ); /** * Set the tagged areas for the image. * It is assumed that the positioned tags have already been set to the ImageInfo * using one of the functions setCategoryInfo or addCategoryInfo. * * @param category the category name. * @param positionedTags a mapping of tag names to image areas. */ void setPositionedTags(const QString& category, const QMap &positionedTags); bool hasCategoryInfo( const QString& key, const QString& value ) const; bool hasCategoryInfo( const QString& key, const StringSet& values ) const; QStringList availableCategories() const; StringSet itemsOfCategory( const QString& category ) const; void renameItem( const QString& key, const QString& oldValue, const QString& newValue ); void renameCategory( const QString& oldName, const QString& newName ); bool operator!=( const ImageInfo& other ) const; bool operator==( const ImageInfo& other ) const; ImageInfo& operator=( const ImageInfo& other ); static bool imageOnDisk( const DB::FileName& fileName ); const MD5& MD5Sum() const { return m_md5sum; } void setMD5Sum( const MD5& sum, bool storeEXIF=true ); void setLocked( bool ); bool isLocked() const; bool isNull() const { return m_null; } QSize size() const; void setSize( const QSize& size ); MediaType mediaType() const; void setMediaType( MediaType type ) { if (type != m_type) m_dirty = true; m_type = type; saveChangesIfNotDelayed(); } bool isVideo() const; void createFolderCategoryItem( DB::CategoryPtr, DB::MemberMap& memberMap ); void delaySavingChanges(bool b=true); void copyExtraData( const ImageInfo& from, bool copyAngle = true); void removeExtraData(); - /** - * Merge another ImageInfo into this one. - * The other ImageInfo is not altered in any way or removed. - */ - void merge(const ImageInfo& other); QMap> taggedAreas() const; /** * Return the area associated with a tag. * @param category the category name * @param tag the tag name * @return the associated area, or QRect() if no association exists. */ QRect areaForTag(QString category, QString tag) const; void setIsMatched(bool isMatched); bool isMatched() const; void setMatchGeneration(int matchGeneration); int matchGeneration() const; #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates coordinates() const; #endif protected: /** Save changes to database. * * Back-ends, which need changes to be instantly in database, * should override this. */ virtual void saveChanges() {} void saveChangesIfNotDelayed() { if (!m_delaySaving) saveChanges(); } void setIsNull(bool b) { m_null = b; } bool isDirty() const { return m_dirty; } void setIsDirty(bool b) { m_dirty = b; } bool updateDateInformation( int mode ) const; void setStackId( const StackID stackId ); friend class XMLDB::Database; private: DB::FileName m_fileName; QString m_label; QString m_description; ImageDate m_date; QMap m_categoryInfomation; QMap> m_taggedAreas; int m_angle; enum OnDisk { YesOnDisk, NoNotOnDisk, Unchecked }; mutable OnDisk m_imageOnDisk; MD5 m_md5sum; bool m_null; QSize m_size; MediaType m_type; short m_rating; StackID m_stackId; unsigned int m_stackOrder; int m_videoLength; bool m_isMatched; int m_matchGeneration; #ifdef HAVE_KGEOMAP mutable KGeoMap::GeoCoordinates m_coordinates; mutable bool m_coordsIsSet = false; #endif // Cache information bool m_locked; // Will be set to true after every change bool m_dirty; bool m_delaySaving; }; } #endif /* IMAGEINFO_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/Database.cpp b/XMLDB/Database.cpp index 69277193..3d034a6d 100644 --- a/XMLDB/Database.cpp +++ b/XMLDB/Database.cpp @@ -1,747 +1,852 @@ /* Copyright (C) 2003-2018 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 "Database.h" #include "Settings/SettingsData.h" #include #include #include "Utilities/Util.h" #include "DB/GroupCounter.h" #include "Browser/BrowserWidget.h" #include "DB/ImageInfo.h" #include "DB/ImageInfoPtr.h" #include "DB/CategoryCollection.h" #include "XMLCategory.h" #include #include #include "XMLImageDateCollection.h" #include "FileReader.h" #include "FileWriter.h" #include "Exif/Database.h" #include +#include "Logging.h" using Utilities::StringSet; bool XMLDB::Database::s_anyImageWithEmptySize = false; XMLDB::Database::Database( const QString& configFile ): m_fileName(configFile) { Utilities::checkForBackupFile( configFile ); FileReader reader( this ); reader.read( configFile ); m_nextStackId = reader.nextStackId(); + // sort out duplicate image entries in index.xml + // Until KPhotoAlbum 5.4, duplicate entries were not checked for. + // As far as I(jzarl) can tell, ignoring this problem is mostly "benign", + // but can cause: + // 1. inconsistencies when searching for tags + // E.g.. a search for tag A shows image B even though the tag has been removed from (one duplicate of) the image + // 2. more duplicates (not sure about that one) + if (!reader.duplicates().isEmpty()) + { + qCWarning(XMLDBLog) << "Database inconsistent: " << reader.duplicates().size() << "duplicate image entries!"; + for (const auto &dupInfo : reader.duplicates()) + { + DB::ImageInfoPtr existingInfo = dupInfo.first; + DB::ImageInfoPtr newInfo = dupInfo.duplicate; + bool hashIsDifferent = (existingInfo->MD5Sum() == newInfo->MD5Sum()); + DB::MD5 preferredHash = newInfo->MD5Sum(); + qCWarning(XMLDBLog) << "Merging duplicate entry for file" << newInfo->fileName().relative(); + mergeImageInfos(existingInfo,newInfo); + if (hashIsDifferent) + { + qCWarning(XMLDBLog).nospace() << "Conflicting information for file " << newInfo->fileName().relative() + << ": duplicate entry with different MD5 sum! Using MD5 sum of newer entry..."; + existingInfo->setMD5Sum(preferredHash, false); + } + } + KMessageBox::information( + 0 + , i18np("

One duplicate image entry was automatically merged.

" + , "

%1 duplicate image entries were automatically merged.

" + , reader.duplicates().size()) + , i18n("Results for automatic repair") + ); + } + connect( categoryCollection(), SIGNAL(itemRemoved(DB::Category*,QString)), this, SLOT(deleteItem(DB::Category*,QString)) ); connect( categoryCollection(), SIGNAL(itemRenamed(DB::Category*,QString,QString)), this, SLOT(renameItem(DB::Category*,QString,QString)) ); connect( categoryCollection(), SIGNAL(itemRemoved(DB::Category*,QString)), &m_members, SLOT(deleteItem(DB::Category*,QString)) ); connect( categoryCollection(), SIGNAL(itemRenamed(DB::Category*,QString,QString)), &m_members, SLOT(renameItem(DB::Category*,QString,QString)) ); connect( categoryCollection(), SIGNAL(categoryRemoved(QString)), &m_members, SLOT(deleteCategory(QString))); } uint XMLDB::Database::totalCount() const { return m_images.count(); } /** * I was considering merging the two calls to this method (one for images, one for video), but then I * realized that all the work is really done after the check for whether the given * imageInfo is of the right type, and as a match can't be both, this really * would buy me nothing. */ QMap XMLDB::Database::classify( const DB::ImageSearchInfo& info, const QString &category, DB::MediaType typemask ) { QMap map; DB::GroupCounter counter( category ); Utilities::StringSet alreadyMatched = info.findAlreadyMatched( category ); DB::ImageSearchInfo noMatchInfo = info; QString currentMatchTxt = noMatchInfo.categoryMatchText( category ); if ( currentMatchTxt.isEmpty() ) noMatchInfo.setCategoryMatchText( category, DB::ImageDB::NONE() ); else noMatchInfo.setCategoryMatchText( category, QString::fromLatin1( "%1 & %2" ).arg(currentMatchTxt).arg(DB::ImageDB::NONE()) ); noMatchInfo.setCacheable( false ); // Iterate through the whole database of images. for( DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it ) { bool match = ( (*it)->mediaType() & typemask ) && !(*it)->isLocked() && info.match( *it ) && rangeInclude( *it ); if ( match ) { // If the given image is currently matched. // Now iterate through all the categories the current image // contains, and increase them in the map mapping from category // to count. StringSet items = (*it)->itemsOfCategory(category); counter.count( items ); for( StringSet::const_iterator it2 = items.begin(); it2 != items.end(); ++it2 ) { if ( !alreadyMatched.contains(*it2) ) // We do not want to match "Jesper & Jesper" map[*it2]++; } // Find those with no other matches if ( noMatchInfo.match( *it ) ) map[DB::ImageDB::NONE()]++; } } QMap groups = counter.result(); for( QMap::iterator it= groups.begin(); it != groups.end(); ++it ) { map[it.key()] = it.value(); } return map; } void XMLDB::Database::renameCategory( const QString& oldName, const QString newName ) { for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it ) { (*it)->renameCategory( oldName, newName ); } } void XMLDB::Database::addToBlockList(const DB::FileNameList& list) { Q_FOREACH(const DB::FileName& fileName, list) { m_blockList.insert(fileName); } deleteList( list ); } void XMLDB::Database::deleteList(const DB::FileNameList& list) { Q_FOREACH(const DB::FileName& fileName, list) { - DB::ImageInfoPtr inf = fileName.info(); + DB::ImageInfoPtr inf = info(fileName); StackMap::iterator found = m_stackMap.find(inf->stackId()); if ( inf->isStacked() && found != m_stackMap.end() ) { const DB::FileNameList origCache = found.value(); DB::FileNameList newCache; Q_FOREACH(const DB::FileName& cacheName, origCache) { if (fileName != cacheName) newCache.append(cacheName); } if (newCache.size() <= 1) { // we're destroying a stack Q_FOREACH(const DB::FileName& cacheName, newCache) { - DB::ImageInfoPtr cacheInf = cacheName.info(); + DB::ImageInfoPtr cacheInf = info(cacheName); cacheInf->setStackId(0); cacheInf->setStackOrder(0); } m_stackMap.remove( inf->stackId() ); } else { m_stackMap.insert(inf->stackId(), newCache); } } m_imageCache.remove( inf->fileName().absolute() ); m_images.remove( inf ); } Exif::Database::instance()->remove( list ); emit totalChanged( m_images.count() ); emit imagesDeleted(list); emit dirty(); } void XMLDB::Database::renameItem( DB::Category* category, const QString& oldName, const QString& newName ) { for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it ) { (*it)->renameItem( category->name(), oldName, newName ); } } void XMLDB::Database::deleteItem( DB::Category* category, const QString& value ) { for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it ) { (*it)->removeCategoryInfo( category->name(), value ); } } void XMLDB::Database::lockDB( bool lock, bool exclude ) { DB::ImageSearchInfo info = Settings::SettingsData::instance()->currentLock(); for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it ) { if ( lock ) { bool match = info.match( *it ); if ( !exclude ) match = !match; (*it)->setLocked( match ); } else (*it)->setLocked( false ); } } void XMLDB::Database::clearDelayedImages() { m_delayedCache.clear(); m_delayedUpdate.clear(); } void XMLDB::Database::forceUpdate( const DB::ImageInfoList& images ) { // FIXME: merge stack information DB::ImageInfoList newImages = images.sort(); if ( m_images.count() == 0 ) { // case 1: The existing imagelist is empty. Q_FOREACH( const DB::ImageInfoPtr& imageInfo, newImages ) m_imageCache.insert( imageInfo->fileName().absolute(), imageInfo ); m_images = newImages; } else if ( newImages.count() == 0 ) { // case 2: No images to merge in - that's easy ;-) return; } else if ( newImages.first()->date().start() > m_images.last()->date().start() ) { // case 2: The new list is later than the existsing Q_FOREACH( const DB::ImageInfoPtr& imageInfo, newImages ) m_imageCache.insert( imageInfo->fileName().absolute(), imageInfo ); m_images.appendList(newImages); } else if ( m_images.isSorted() ) { // case 3: The lists overlaps, and the existsing list is sorted Q_FOREACH( const DB::ImageInfoPtr& imageInfo, newImages ) m_imageCache.insert( imageInfo->fileName().absolute(), imageInfo ); m_images.mergeIn( newImages ); } else{ // case 4: The lists overlaps, and the existsing list is not sorted in the overlapping range. Q_FOREACH( const DB::ImageInfoPtr& imageInfo, newImages ) m_imageCache.insert( imageInfo->fileName().absolute(), imageInfo ); m_images.appendList( newImages ); } } void XMLDB::Database::addImages( const DB::ImageInfoList& images, bool doUpdate ) { Q_FOREACH( const DB::ImageInfoPtr& info, images ) { info->addCategoryInfo( i18n( "Media Type" ), info->mediaType() == DB::Image ? i18n( "Image" ) : i18n( "Video" ) ); m_delayedCache.insert( info->fileName().absolute(), info ); m_delayedUpdate << info; } if ( doUpdate ) { commitDelayedImages(); } } void XMLDB::Database::commitDelayedImages() { uint imagesAdded = m_delayedUpdate.count(); if ( imagesAdded > 0 ) { forceUpdate(m_delayedUpdate); m_delayedCache.clear(); m_delayedUpdate.clear(); // It's the responsibility of the caller to add the Exif information. // It's more efficient from an I/O perspective to minimize the number // of passes over the images, and with the ability to add the Exif // data in a transaction, there's no longer any need to read it here. emit totalChanged( m_images.count() ); emit dirty(); } } void XMLDB::Database::renameImage( DB::ImageInfoPtr info, const DB::FileName& newName ) { info->delaySavingChanges(false); info->setFileName(newName); } DB::ImageInfoPtr XMLDB::Database::info( const DB::FileName& fileName ) const { if ( fileName.isNull() ) return DB::ImageInfoPtr(); const QString name = fileName.absolute(); if (m_imageCache.contains( name )) return m_imageCache[name]; if (m_delayedCache.contains( name )) return m_delayedCache[name]; Q_FOREACH( const DB::ImageInfoPtr& imageInfo, m_images ) m_imageCache.insert( imageInfo->fileName().absolute(), imageInfo ); if ( m_imageCache.contains( name ) ) { return m_imageCache[ name ]; } return DB::ImageInfoPtr(); } bool XMLDB::Database::rangeInclude( DB::ImageInfoPtr info ) const { if (m_selectionRange.start().isNull() ) return true; DB::ImageDate::MatchType tp = info->date().isIncludedIn( m_selectionRange ); if ( m_includeFuzzyCounts ) return ( tp == DB::ImageDate::ExactMatch || tp == DB::ImageDate::RangeMatch ); else return ( tp == DB::ImageDate::ExactMatch ); } DB::MemberMap& XMLDB::Database::memberMap() { return m_members; } void XMLDB::Database::save( const QString& fileName, bool isAutoSave ) { FileWriter saver( this ); saver.save( fileName, isAutoSave ); } DB::MD5Map* XMLDB::Database::md5Map() { return &m_md5map; } bool XMLDB::Database::isBlocking( const DB::FileName& fileName ) { return m_blockList.contains( fileName ); } DB::FileNameList XMLDB::Database::images() { return m_images.files(); } DB::FileNameList XMLDB::Database::search( const DB::ImageSearchInfo& info, bool requireOnDisk) const { return searchPrivate( info, requireOnDisk, true ); } DB::FileNameList XMLDB::Database::searchPrivate( const DB::ImageSearchInfo& info, bool requireOnDisk, bool onlyItemsMatchingRange) const { // When searching for images counts for the datebar, we want matches outside the range too. // When searching for images for the thumbnail view, we only want matches inside the range. DB::FileNameList result; for( DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it ) { bool match = !(*it)->isLocked() && info.match( *it ) && ( !onlyItemsMatchingRange || rangeInclude( *it )); match &= !requireOnDisk || DB::ImageInfo::imageOnDisk( (*it)->fileName() ); if (match) result.append((*it)->fileName()); } return result; } void XMLDB::Database::sortAndMergeBackIn(const DB::FileNameList& fileNameList) { DB::ImageInfoList infoList; Q_FOREACH( const DB::FileName &fileName, fileNameList ) - infoList.append(fileName.info()); + infoList.append(info(fileName)); m_images.sortAndMergeBackIn(infoList); } DB::CategoryCollection* XMLDB::Database::categoryCollection() { return &m_categoryCollection; } QExplicitlySharedDataPointer XMLDB::Database::rangeCollection() { return QExplicitlySharedDataPointer( new XMLImageDateCollection( searchPrivate( Browser::BrowserWidget::instance()->currentContext(), false, false))); } void XMLDB::Database::reorder( const DB::FileName& item, const DB::FileNameList& selection, bool after) { Q_ASSERT(!item.isNull()); DB::ImageInfoList list = takeImagesFromSelection(selection); insertList(item, list, after ); } // Remove all the images from the database that match the given selection and // return that sublist. // This returns the selected and erased images in the order in which they appear // in the image list itself. DB::ImageInfoList XMLDB::Database::takeImagesFromSelection(const DB::FileNameList& selection) { DB::ImageInfoList result; if (selection.isEmpty()) return result; // iterate over all images (expensive!!) TODO: improve? for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); /**/ ) { const DB::FileName imagefile = (*it)->fileName(); DB::FileNameList::const_iterator si = selection.begin(); // for each image, iterate over selection, break on match for ( /**/; si != selection.end(); ++si ) { const DB::FileName file = *si; if ( imagefile == file ) { break; } } // if image is not in selection, simply advance to next, if not add to result and erase if (si == selection.end()) { ++it; } else { result << *it; m_imageCache.remove( (*it)->fileName().absolute() ); it = m_images.erase(it); } // if all images from selection are in result (size of lists is equal) break. if (result.size() == selection.size()) break; } return result; } void XMLDB::Database::insertList( const DB::FileName& fileName, const DB::ImageInfoList& list, bool after) { DB::ImageInfoListIterator imageIt = m_images.begin(); for( ; imageIt != m_images.end(); ++imageIt ) { if ( (*imageIt)->fileName() == fileName ) { break; } } // since insert() inserts before iterator increment when inserting AFTER image if ( after ) imageIt++; for( DB::ImageInfoListConstIterator it = list.begin(); it != list.end(); ++it ) { // the call to insert() destroys the given iterator so use the new one after the call imageIt = m_images.insert( imageIt, *it ); m_imageCache.insert( (*it)->fileName().absolute(), *it ); // increment always to retain order of selected images imageIt++; } emit dirty(); } bool XMLDB::Database::stack(const DB::FileNameList& items) { unsigned int changed = 0; QSet stacks; QList images; unsigned int stackOrder = 1; Q_FOREACH(const DB::FileName& fileName, items) { - DB::ImageInfoPtr imgInfo = fileName.info(); + DB::ImageInfoPtr imgInfo = info(fileName); Q_ASSERT( imgInfo ); if ( imgInfo->isStacked() ) { stacks << imgInfo->stackId(); stackOrder = qMax( stackOrder, imgInfo->stackOrder() + 1 ); } else { images << imgInfo; } } if ( stacks.size() > 1 ) return false; // images already in different stacks -> can't stack DB::StackID stackId = ( stacks.size() == 1 ) ? *(stacks.begin() ) : m_nextStackId++; Q_FOREACH( DB::ImageInfoPtr info, images ) { info->setStackOrder( stackOrder ); info->setStackId( stackId ); m_stackMap[stackId].append(info->fileName()); ++changed; ++stackOrder; } if ( changed ) emit dirty(); return changed; } void XMLDB::Database::unstack(const DB::FileNameList& items) { Q_FOREACH(const DB::FileName& fileName, items) { DB::FileNameList allInStack = getStackFor(fileName); if (allInStack.size() <= 2) { // we're destroying stack here Q_FOREACH(const DB::FileName& stackFileName, allInStack) { - DB::ImageInfoPtr imgInfo = stackFileName.info(); + DB::ImageInfoPtr imgInfo = info(stackFileName); Q_ASSERT( imgInfo ); if ( imgInfo->isStacked() ) { m_stackMap.remove( imgInfo->stackId() ); imgInfo->setStackId( 0 ); imgInfo->setStackOrder( 0 ); } } } else { - DB::ImageInfoPtr imgInfo = fileName.info(); + DB::ImageInfoPtr imgInfo = info(fileName); Q_ASSERT( imgInfo ); if ( imgInfo->isStacked() ) { m_stackMap[imgInfo->stackId()].removeAll(fileName); imgInfo->setStackId( 0 ); imgInfo->setStackOrder( 0 ); } } } if (!items.isEmpty()) emit dirty(); } DB::FileNameList XMLDB::Database::getStackFor(const DB::FileName& referenceImg) const { DB::ImageInfoPtr imageInfo = info( referenceImg ); if ( !imageInfo || ! imageInfo->isStacked() ) return DB::FileNameList(); StackMap::iterator found = m_stackMap.find(imageInfo->stackId()); if ( found != m_stackMap.end() ) return found.value(); // it wasn't in the cache -> rebuild it m_stackMap.clear(); for( DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it ) { if ( (*it)->isStacked() ) { DB::StackID stackid = (*it)->stackId(); m_stackMap[stackid].append((*it)->fileName()); } } found = m_stackMap.find(imageInfo->stackId()); if ( found != m_stackMap.end() ) return found.value(); else return DB::FileNameList(); } void XMLDB::Database::copyData(const DB::FileName &from, const DB::FileName &to) { - (*info(to)).merge(*info(from)); + mergeImageInfos(info(to), info(from)); +} + +void XMLDB::Database::mergeImageInfos(DB::ImageInfoPtr toInfo, const DB::ImageInfoPtr fromInfo) +{ + // Merge date + if ( fromInfo->date() != toInfo->m_date) + { + // a fuzzy date has been set by the user and therefore "wins" over an exact date. + // two fuzzy dates can be merged + // two exact dates should ideally be cross-checked with Exif information in the file. + // Nevertheless, we merge them into a fuzzy date to avoid the complexity of checking the file. + if (fromInfo->date().isFuzzy()) + { + if (toInfo->m_date.isFuzzy()) + toInfo->m_date.extendTo(fromInfo->date()); + else + toInfo->m_date = fromInfo->date(); + } + else if (!toInfo->m_date.isFuzzy()) + { + toInfo->m_date.extendTo(fromInfo->date()); + } + // else: keep m_date + } + + // Merge description + if ( !fromInfo->description().isEmpty() ) { + if ( toInfo->m_description.isEmpty() ) + toInfo->m_description = fromInfo->description(); + else if (toInfo->m_description != fromInfo->description()) + toInfo->m_description += QString::fromUtf8("\n-----------\n") + fromInfo->m_description; + } + + // Clear untagged tag if only one of the images was untagged + const QString untaggedCategory = Settings::SettingsData::instance()->untaggedCategory(); + const QString untaggedTag = Settings::SettingsData::instance()->untaggedTag(); + const bool isCompleted = !toInfo->m_categoryInfomation[untaggedCategory].contains(untaggedTag) || !fromInfo->m_categoryInfomation[untaggedCategory].contains(untaggedTag); + + // Merge tags + QSet keys = QSet::fromList(toInfo->m_categoryInfomation.keys()); + keys.unite(QSet::fromList(fromInfo->m_categoryInfomation.keys())); + for( const QString& key : keys) { + toInfo->m_categoryInfomation[key].unite(fromInfo->m_categoryInfomation[key]); + } + + // Clear untagged tag if only one of the images was untagged + if (isCompleted) + toInfo->m_categoryInfomation[untaggedCategory].remove(untaggedTag); + + // merge stacks: + if (toInfo->isStacked() || fromInfo->isStacked()) + { + if (toInfo->stackId() != fromInfo->stackId()) + { + DB::FileNameList stackImages; + if (!toInfo->isStacked()) + stackImages.append(toInfo->fileName()); + else + stackImages.append(getStackFor(toInfo->fileName())); + stackImages.append(getStackFor(fromInfo->fileName())); + + unstack(stackImages); + if (!stack(stackImages)) + qCWarning(XMLDBLog, "Could not merge stacks!"); + } + else + { + qCInfo(XMLDBLog, "Stack id identical on merge."); + } + } } int XMLDB::Database::fileVersion() { // File format version, bump it up every time the format for the file changes. return 8; } // During profiling of loading, I found that a significant amount of time was spent in QDateTime::fromString. // Reviewing the code, I fount that it did a lot of extra checks we don't need (like checking if the string have // timezone information (which they won't in KPA), this function is a replacement that is faster than the original. QDateTime dateTimeFromString(const QString& str) { static QChar T = QChar::fromLatin1('T'); if ( str[10] == T) return QDateTime(QDate::fromString(str.left(10), Qt::ISODate),QTime::fromString(str.mid(11),Qt::ISODate)); else return QDateTime::fromString(str,Qt::ISODate); } DB::ImageInfoPtr XMLDB::Database::createImageInfo( const DB::FileName& fileName, ReaderPtr reader, Database* db, const QMap *newToOldCategory ) { static QString _label_ = QString::fromUtf8("label"); static QString _description_ = QString::fromUtf8("description"); static QString _startDate_ = QString::fromUtf8("startDate"); static QString _endDate_ = QString::fromUtf8("endDate"); static QString _yearFrom_ = QString::fromUtf8("yearFrom"); static QString _monthFrom_ = QString::fromUtf8("monthFrom"); static QString _dayFrom_ = QString::fromUtf8("dayFrom"); static QString _hourFrom_ = QString::fromUtf8("hourFrom"); static QString _minuteFrom_ = QString::fromUtf8("minuteFrom"); static QString _secondFrom_ = QString::fromUtf8("secondFrom"); static QString _yearTo_ = QString::fromUtf8("yearTo"); static QString _monthTo_ = QString::fromUtf8("monthTo"); static QString _dayTo_ = QString::fromUtf8("dayTo"); static QString _angle_ = QString::fromUtf8("angle"); static QString _md5sum_ = QString::fromUtf8("md5sum"); static QString _width_ = QString::fromUtf8("width"); static QString _height_ = QString::fromUtf8("height"); static QString _rating_ = QString::fromUtf8("rating"); static QString _stackId_ = QString::fromUtf8("stackId"); static QString _stackOrder_ = QString::fromUtf8("stackOrder"); static QString _gpsPrec_ = QString::fromUtf8("gpsPrec"); static QString _gpsLon_ = QString::fromUtf8("gpsLon"); static QString _gpsLat_ = QString::fromUtf8("gpsLat"); static QString _gpsAlt_ = QString::fromUtf8("gpsAlt"); static QString _videoLength_ = QString::fromUtf8("videoLength"); static QString _options_ = QString::fromUtf8("options"); static QString _0_ = QString::fromUtf8("0"); static QString _minus1_ = QString::fromUtf8("-1"); static QString _MediaType_ = i18n("Media Type"); static QString _Image_ = i18n("Image"); static QString _Video_ = i18n("Video"); QString label; if (reader->hasAttribute(_label_)) label = reader->attribute(_label_); else label = QFileInfo(fileName.relative()).completeBaseName(); QString description; if ( reader->hasAttribute(_description_) ) description = reader->attribute(_description_); DB::ImageDate date; if ( reader->hasAttribute(_startDate_) ) { QDateTime start; QString str = reader->attribute( _startDate_ ); if ( !str.isEmpty() ) start = dateTimeFromString( str ); str = reader->attribute( _endDate_ ); if ( !str.isEmpty() ) date = DB::ImageDate( start, dateTimeFromString(str) ); else date = DB::ImageDate( start ); } else { int yearFrom = 0, monthFrom = 0, dayFrom = 0, yearTo = 0, monthTo = 0, dayTo = 0, hourFrom = -1, minuteFrom = -1, secondFrom = -1; yearFrom = reader->attribute( _yearFrom_, _0_ ).toInt(); monthFrom = reader->attribute( _monthFrom_, _0_ ).toInt(); dayFrom = reader->attribute( _dayFrom_, _0_ ).toInt(); hourFrom = reader->attribute( _hourFrom_, _minus1_ ).toInt(); minuteFrom = reader->attribute( _minuteFrom_, _minus1_ ).toInt(); secondFrom = reader->attribute( _secondFrom_, _minus1_ ).toInt(); yearTo = reader->attribute( _yearTo_, _0_ ).toInt(); monthTo = reader->attribute( _monthTo_, _0_ ).toInt(); dayTo = reader->attribute( _dayTo_, _0_ ).toInt(); date = DB::ImageDate( yearFrom, monthFrom, dayFrom, yearTo, monthTo, dayTo, hourFrom, minuteFrom, secondFrom ); } int angle = reader->attribute( _angle_, _0_).toInt(); DB::MD5 md5sum(reader->attribute( _md5sum_ )); s_anyImageWithEmptySize |= !reader->hasAttribute(_width_); int w = reader->attribute( _width_ , _minus1_ ).toInt(); int h = reader->attribute( _height_ , _minus1_ ).toInt(); QSize size = QSize( w,h ); DB::MediaType mediaType = Utilities::isVideo(fileName) ? DB::Video : DB::Image; short rating = reader->attribute( _rating_, _minus1_ ).toShort(); DB::StackID stackId = reader->attribute( _stackId_, _0_ ).toULong(); unsigned int stackOrder = reader->attribute( _stackOrder_, _0_ ).toULong(); DB::ImageInfo* info = new DB::ImageInfo( fileName, label, description, date, angle, md5sum, size, mediaType, rating, stackId, stackOrder ); if ( reader->hasAttribute(_videoLength_)) info->setVideoLength(reader->attribute(_videoLength_).toInt()); DB::ImageInfoPtr result(info); possibleLoadCompressedCategories( reader, result, db, newToOldCategory ); while( reader->readNextStartOrStopElement(_options_).isStartToken) { readOptions( result, reader, newToOldCategory ); } info->addCategoryInfo( _MediaType_, info->mediaType() == DB::Image ? _Image_ : _Video_ ); return result; } void XMLDB::Database::readOptions( DB::ImageInfoPtr info, ReaderPtr reader, const QMap *newToOldCategory ) { static QString _name_ = QString::fromUtf8("name"); static QString _value_ = QString::fromUtf8("value"); static QString _option_ = QString::fromUtf8("option"); static QString _area_ = QString::fromUtf8("area"); while (reader->readNextStartOrStopElement(_option_).isStartToken) { QString name = FileReader::unescape( reader->attribute(_name_) ); // If the silent update to db version 6 has been done, use the updated category names. if (newToOldCategory) { name = newToOldCategory->key(name,name); } if ( !name.isNull() ) { // Read values while (reader->readNextStartOrStopElement(_value_).isStartToken) { QString value = reader->attribute(_value_); if (reader->hasAttribute(_area_)) { QStringList areaData = reader->attribute(_area_).split(QString::fromUtf8(" ")); int x = areaData[0].toInt(); int y = areaData[1].toInt(); int w = areaData[2].toInt(); int h = areaData[3].toInt(); QRect area = QRect(QPoint(x, y), QPoint(x + w - 1, y + h - 1)); if (! value.isNull()) { info->addCategoryInfo(name, value, area); } } else { if (! value.isNull()) { info->addCategoryInfo(name, value); } } reader->readEndElement(); } } } } void XMLDB::Database::possibleLoadCompressedCategories( ReaderPtr reader, DB::ImageInfoPtr info, Database* db, const QMap *newToOldCategory ) { if ( db == nullptr ) return; Q_FOREACH( const DB::CategoryPtr categoryPtr, db->m_categoryCollection.categories() ) { QString categoryName = categoryPtr->name(); QString oldCategoryName; if ( newToOldCategory ) { // translate to old categoryName, defaulting to the original name if not found: oldCategoryName = newToOldCategory->value( categoryName, categoryName ); } else { oldCategoryName = categoryName; } QString str = reader->attribute( FileWriter::escape( oldCategoryName ) ); if ( !str.isEmpty() ) { QStringList list = str.split(QString::fromLatin1( "," ), QString::SkipEmptyParts ); Q_FOREACH( const QString &tagString, list ) { int id = tagString.toInt(); QString name = static_cast(categoryPtr.data())->nameForId(id); info->addCategoryInfo( categoryName, name ); } } } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/Database.h b/XMLDB/Database.h index a58ed673..2f09f463 100644 --- a/XMLDB/Database.h +++ b/XMLDB/Database.h @@ -1,130 +1,147 @@ /* 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 XMLDB_DATABASE_H #define XMLDB_DATABASE_H #include "DB/ImageSearchInfo.h" #include "DB/ImageInfoList.h" #include #include "DB/MemberMap.h" #include "DB/ImageDB.h" #include "DB/Category.h" #include "DB/CategoryCollection.h" #include "XMLCategoryCollection.h" #include "DB/MD5Map.h" #include #include #include "FileReader.h" namespace DB { class ImageInfo; } namespace XMLDB { class Database :public DB::ImageDB { Q_OBJECT public: uint totalCount() const override; DB::FileNameList search( const DB::ImageSearchInfo&, bool requireOnDisk=false) const override; void renameCategory( const QString& oldName, const QString newName ) override; QMap classify( const DB::ImageSearchInfo& info, const QString &category, DB::MediaType typemask ) override; DB::FileNameList images() override; void addImages( const DB::ImageInfoList& images, bool doUpdate ) override; void commitDelayedImages() override; void clearDelayedImages() override; void renameImage( DB::ImageInfoPtr info, const DB::FileName& newName ) override; void addToBlockList(const DB::FileNameList& list) override; bool isBlocking( const DB::FileName& fileName ) override; void deleteList(const DB::FileNameList& list) override; DB::ImageInfoPtr info( const DB::FileName& fileName ) const override; DB::MemberMap& memberMap() override; void save( const QString& fileName, bool isAutoSave ) override; DB::MD5Map* md5Map() override; void sortAndMergeBackIn(const DB::FileNameList& idList) override; DB::CategoryCollection* categoryCollection() override; QExplicitlySharedDataPointer rangeCollection() override; void reorder( const DB::FileName& item, const DB::FileNameList& cutList, bool after) override; static DB::ImageInfoPtr createImageInfo( const DB::FileName& fileName, ReaderPtr, Database* db = nullptr, const QMap *newToOldCategory = nullptr ); static void possibleLoadCompressedCategories( ReaderPtr reader , DB::ImageInfoPtr info, Database* db, const QMap *newToOldCategory = nullptr ); bool stack(const DB::FileNameList& items) override; void unstack(const DB::FileNameList& images) override; DB::FileNameList getStackFor(const DB::FileName& referenceId) const override; void copyData( const DB::FileName& from, const DB::FileName& to) override; + /** + * @brief mergeImageInfos merges two ImageInfos. + * This is how information is merged: + * - fuzzy dates are extended, precise dates become date ranges + * (if only one date is fuzzy, the fuzzy date prevails) + * - descriptions are merged + * - the label of the \c toInfo is retained + * - tags are merged + * (if only one image is marked as untagged, the untagged tag is removed) + * - stacks are merged + * - the MD5 of the \c toInfo is retained + * + * @param toInfo + * @param fromInfo + */ + void mergeImageInfos( DB::ImageInfoPtr toInfo, DB::ImageInfoPtr const fromInfo); + static int fileVersion(); protected: DB::FileNameList searchPrivate( const DB::ImageSearchInfo&, bool requireOnDisk, bool onlyItemsMatchingRange) const; bool rangeInclude( DB::ImageInfoPtr info ) const; DB::ImageInfoList takeImagesFromSelection(const DB::FileNameList& list); void insertList( const DB::FileName& id, const DB::ImageInfoList& list, bool after ); static void readOptions( DB::ImageInfoPtr info, ReaderPtr reader, const QMap *newToOldCategory = nullptr ); protected slots: void renameItem( DB::Category* category, const QString& oldName, const QString& newName ); void deleteItem( DB::Category* category, const QString& option ); void lockDB( bool lock, bool exclude ) override; private: friend class DB::ImageDB; friend class FileReader; friend class FileWriter; Database( const QString& configFile ); void forceUpdate( const DB::ImageInfoList& ); QString m_fileName; DB::ImageInfoList m_images; QSet m_blockList; DB::ImageInfoList m_missingTimes; XMLCategoryCollection m_categoryCollection; DB::MemberMap m_members; DB::MD5Map m_md5map; //QMap m_settings; DB::StackID m_nextStackId; typedef QMap StackMap; mutable StackMap m_stackMap; DB::ImageInfoList m_delayedUpdate; mutable QHash m_imageCache; mutable QHash m_delayedCache; // used for checking if any images are without image attribute from the database. static bool s_anyImageWithEmptySize; }; } #endif /* XMLDB_DATABASE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileReader.cpp b/XMLDB/FileReader.cpp index 2a4d1c78..35065e15 100644 --- a/XMLDB/FileReader.cpp +++ b/XMLDB/FileReader.cpp @@ -1,588 +1,549 @@ /* Copyright (C) 2003-2015 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. */ // Local includes #include "CompressFileInfo.h" #include "Database.h" #include "FileReader.h" #include "Logging.h" #include "XMLCategory.h" #include #include #include #include // KDE includes #include #include // Qt includes #include #include #include #include #include #include -namespace { -/** - * @brief mergeInfos merges two duplicate ImageInfos. - * If the MD5 sum differs, the "newer" one is used. - * All other properties are merged. - * - * Until KPhotoAlbum 5.4, duplicate entries were not checked for. - * As far as I(jzarl) can tell, ignoring this problem is mostly "benign", - * but can cause: - * 1. inconsistencies when searching for tags - * E.g.. a search for tag A shows image B even though the tag has been removed from (one duplicate of) the image - * 2. more duplicates (not sure about that one) - * - * @param existingInfo - * @param newInfo - * @return \c true, if the items could be merged, \c false otherwise. - */ -void mergeInfos(DB::ImageInfoPtr existingInfo, DB::ImageInfoPtr newInfo) -{ - bool hashIsDifferent = (existingInfo->MD5Sum() == newInfo->MD5Sum()); - DB::MD5 preferredHash = newInfo->MD5Sum(); - qCWarning(XMLDBLog) << "Merging duplicate entry for file" << newInfo->fileName().relative(); - existingInfo->merge(*newInfo); - if (hashIsDifferent) - { - qCWarning(XMLDBLog).nospace() << "Conflicting information for file " << newInfo->fileName().relative() - << ": duplicate entry with different MD5 sum! Using MD5 sum of newer entry..."; - existingInfo->setMD5Sum(preferredHash, false); - } -} -} // namespace void XMLDB::FileReader::read( const QString& configFile ) { static QString versionString = QString::fromUtf8("version"); static QString compressedString = QString::fromUtf8("compressed"); ReaderPtr reader = readConfigFile( configFile ); ElementInfo info = reader->readNextStartOrStopElement(QString::fromUtf8("KPhotoAlbum")); if (!info.isStartToken) reader->complainStartElementExpected(QString::fromUtf8("KPhotoAlbum")); m_fileVersion = reader->attribute( versionString, QString::fromLatin1( "1" ) ).toInt(); if ( m_fileVersion > Database::fileVersion() ) { int ret = KMessageBox::warningContinueCancel( messageParent(), i18n("

The database file (index.xml) is from a newer version of KPhotoAlbum!

" "

Chances are you will be able to read this file, but when writing it back, " "information saved in the newer version will be lost

"), i18n("index.xml version mismatch"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString::fromLatin1( "checkDatabaseFileVersion" ) ); if (ret == KStandardGuiItem::Cancel) exit(-1); } setUseCompressedFileFormat( reader->attribute(compressedString).toInt() ); m_db->m_members.setLoading( true ); loadCategories( reader ); loadImages( reader ); loadBlockList( reader ); loadMemberGroups( reader ); //loadSettings(reader); m_db->m_members.setLoading( false ); checkIfImagesAreSorted(); checkIfAllImagesHaveSizeAttributes(); } void XMLDB::FileReader::createSpecialCategories() { // Setup the "Folder" category m_folderCategory = new XMLCategory(i18n("Folder"), QString::fromLatin1("folder"), DB::Category::TreeView, 32, false ); m_folderCategory->setType( DB::Category::FolderCategory ); // The folder category is not stored in the index.xml file, // but older versions of KPhotoAlbum stored a stub entry, which we need to remove first: if ( m_db->m_categoryCollection.categoryForName(m_folderCategory->name()) ) m_db->m_categoryCollection.removeCategory(m_folderCategory->name()); m_db->m_categoryCollection.addCategory( m_folderCategory ); dynamic_cast( m_folderCategory.data() )->setShouldSave( false ); // Setup the "Tokens" category DB::CategoryPtr tokenCat; if (m_fileVersion >= 7) { tokenCat = m_db->m_categoryCollection.categoryForSpecial( DB::Category::TokensCategory ); } else { // Before version 7, the "Tokens" category name wasn't stored to the settings. So ... // look for a literal "Tokens" category ... tokenCat = m_db->m_categoryCollection.categoryForName(QString::fromUtf8("Tokens")); if (!tokenCat) { // ... and a translated "Tokens" category if we don't have the literal one. tokenCat = m_db->m_categoryCollection.categoryForName(i18n("Tokens")); } if (tokenCat) { // in this case we need to give the tokens category its special meaning: m_db->m_categoryCollection.removeCategory(tokenCat->name()); tokenCat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(tokenCat); } } if (! tokenCat) { // Create a new "Tokens" category tokenCat = new XMLCategory(i18n("Tokens"), QString::fromUtf8("tag"), DB::Category::TreeView, 32, true); tokenCat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(tokenCat); } // KPhotoAlbum 2.2 did not write the tokens to the category section, // so unless we do this small trick they will not show up when importing. for (char ch = 'A'; ch <= 'Z'; ++ch) { tokenCat->addItem(QString::fromUtf8("%1").arg(QChar::fromLatin1(ch))); } // Setup the "Media Type" category DB::CategoryPtr mediaCat; mediaCat = new XMLCategory(i18n("Media Type"), QString::fromLatin1("view-categories"), DB::Category::TreeView, 32, false); mediaCat->addItem( i18n( "Image" ) ); mediaCat->addItem( i18n( "Video" ) ); mediaCat->setType( DB::Category::MediaTypeCategory ); dynamic_cast( mediaCat.data() )->setShouldSave( false ); // The media type is not stored in the media category, // but older versions of KPhotoAlbum stored a stub entry, which we need to remove first: if ( m_db->m_categoryCollection.categoryForName(mediaCat->name()) ) m_db->m_categoryCollection.removeCategory( mediaCat->name() ); m_db->m_categoryCollection.addCategory( mediaCat ); } void XMLDB::FileReader::loadCategories( ReaderPtr reader ) { static QString nameString = QString::fromUtf8("name"); static QString iconString = QString::fromUtf8("icon"); static QString viewTypeString = QString::fromUtf8("viewtype"); static QString showString = QString::fromUtf8("show"); static QString thumbnailSizeString = QString::fromUtf8("thumbnailsize"); static QString positionableString = QString::fromUtf8("positionable"); static QString metaString = QString::fromUtf8("meta"); static QString tokensString = QString::fromUtf8("tokens"); static QString valueString = QString::fromUtf8("value"); static QString idString = QString::fromUtf8("id"); static QString birthDateString = QString::fromUtf8("birthDate"); static QString categoriesString = QString::fromUtf8("Categories"); static QString categoryString = QString::fromUtf8("Category"); ElementInfo info = reader->readNextStartOrStopElement(categoriesString); if (!info.isStartToken) reader->complainStartElementExpected(categoriesString); while ( reader->readNextStartOrStopElement(categoryString).isStartToken) { const QString categoryName = unescape(reader->attribute(nameString)); if ( !categoryName.isNull() ) { // Read Category info QString icon = reader->attribute(iconString); DB::Category::ViewType type = (DB::Category::ViewType) reader->attribute( viewTypeString, QString::fromLatin1( "0" ) ).toInt(); int thumbnailSize = reader->attribute( thumbnailSizeString, QString::fromLatin1( "32" ) ).toInt(); bool show = (bool) reader->attribute( showString, QString::fromLatin1( "1" ) ).toInt(); bool positionable = (bool) reader->attribute( positionableString, QString::fromLatin1( "0" ) ).toInt(); bool tokensCat = reader->attribute(metaString) == tokensString; DB::CategoryPtr cat = m_db->m_categoryCollection.categoryForName( categoryName ); bool repairMode = false; if (cat) { KMessageBox::ButtonCode choice = KMessageBox::warningContinueCancel( messageParent(), i18n( "

Line %1, column %2: duplicate category '%3'

" "

Choose continue to ignore the duplicate category and try an automatic repair, " "or choose cancel to quit.

", reader->lineNumber(), reader->columnNumber(), categoryName ), i18n("Error in database file")); if ( choice == KMessageBox::Continue ) repairMode = true; else exit(-1); } else { cat = new XMLCategory( categoryName, icon, type, thumbnailSize, show, positionable ); if (tokensCat) cat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory( cat ); } // Read values QStringList items; while( reader->readNextStartOrStopElement(valueString).isStartToken) { QString value = reader->attribute(valueString); if ( reader->hasAttribute(idString) ) { int id = reader->attribute(idString).toInt(); static_cast(cat.data())->setIdMapping( value, id ); } if (reader->hasAttribute(birthDateString)) cat->setBirthDate(value,QDate::fromString(reader->attribute(birthDateString), Qt::ISODate)); items.append( value ); reader->readEndElement(); } if ( repairMode ) { // merge with duplicate category qCInfo(XMLDBLog) << "Repairing category " << categoryName << ": merging items " << cat->items() << " with " << items; items.append(cat->items()); items.removeDuplicates(); } cat->setItems( items ); } } createSpecialCategories(); if (m_fileVersion < 7) { KMessageBox::information( messageParent(), i18nc("Leave \"Folder\" and \"Media Type\" untranslated below, those will show up with " "these exact names. Thanks :-)", "

This version of KPhotoAlbum does not translate \"standard\" categories " "any more.

" "

This may mean that – if you use a locale other than English – some of your " "categories are now displayed in English.

" "

You can manually rename your categories any time and then save your database." "

" "

In some cases, you may get two additional empty categories, \"Folder\" and " "\"Media Type\". You can delete those.

"), i18n("Changed standard category names") ); } } void XMLDB::FileReader::loadImages( ReaderPtr reader ) { static QString fileString = QString::fromUtf8("file"); static QString imagesString = QString::fromUtf8("images"); static QString imageString = QString::fromUtf8("image"); ElementInfo info = reader->readNextStartOrStopElement(imagesString); if (!info.isStartToken) reader->complainStartElementExpected(imagesString); - int repair_numDuplicateInfos = 0; while (reader->readNextStartOrStopElement(imageString).isStartToken) { const QString fileNameStr = reader->attribute(fileString); if ( fileNameStr.isNull() ) { qCWarning(XMLDBLog, "Element did not contain a file attribute" ); return; } const DB::FileName dbFileName = DB::FileName::fromRelativePath(fileNameStr); DB::ImageInfoPtr info = load( dbFileName, reader ); if ( m_db->md5Map()->containsFile(dbFileName) ) { DB::ImageInfoPtr existingInfo = m_db->info(dbFileName); - repair_numDuplicateInfos++; - mergeInfos(existingInfo, info); + // store duplicate for later + // at this point, the database is not fully initialized and if the info + // is part of a stack it is possible that not all images of the stack are + // known at this point. + m_duplicateList.append(DuplicateInfo{existingInfo,info}); } else { m_db->m_images.append(info); m_db->m_md5map.insert( info->MD5Sum(), dbFileName ); } } - if (repair_numDuplicateInfos>0) - { - KMessageBox::information( - messageParent() - , i18np("

One duplicate image entry was automatically merged.

" - , "

%1 duplicate image entries were automatically merged.

" - , repair_numDuplicateInfos) - , i18n("Results for automatic repair") - ); - } } void XMLDB::FileReader::loadBlockList( ReaderPtr reader ) { static QString fileString = QString::fromUtf8("file"); static QString blockListString = QString::fromUtf8("blocklist"); static QString blockString = QString::fromUtf8("block"); ElementInfo info = reader->peekNext(); if ( info.isStartToken && info.tokenName == blockListString ) { reader->readNextStartOrStopElement(blockListString); while (reader->readNextStartOrStopElement(blockString).isStartToken) { QString fileName = reader->attribute(fileString); if ( !fileName.isEmpty() ) m_db->m_blockList.insert(DB::FileName::fromRelativePath(fileName)); reader->readEndElement(); } } } void XMLDB::FileReader::loadMemberGroups( ReaderPtr reader ) { static QString categoryString = QString::fromUtf8("category"); static QString groupNameString = QString::fromUtf8("group-name"); static QString memberString = QString::fromUtf8("member"); static QString membersString = QString::fromUtf8("members"); static QString memberGroupsString = QString::fromUtf8("member-groups"); ElementInfo info = reader->peekNext(); if ( info.isStartToken && info.tokenName == memberGroupsString) { reader->readNextStartOrStopElement(memberGroupsString); while(reader->readNextStartOrStopElement(memberString).isStartToken) { QString category = reader->attribute(categoryString); QString group = reader->attribute(groupNameString); if ( reader->hasAttribute(memberString) ) { QString member = reader->attribute(memberString); m_db->m_members.addMemberToGroup( category, group, member ); } else { QStringList members = reader->attribute(membersString).split( QString::fromLatin1( "," ), QString::SkipEmptyParts ); Q_FOREACH( const QString &memberItem, members ) { DB::CategoryPtr catPtr = m_db->m_categoryCollection.categoryForName( category ); if (!catPtr) { // category was not declared in "Categories" qCWarning(XMLDBLog) << "File corruption in index.xml. Inserting missing category: " << category; catPtr = new XMLCategory(category, QString::fromUtf8("dialog-warning"), DB::Category::TreeView, 32, false); m_db->m_categoryCollection.addCategory( catPtr ); } XMLCategory* cat = static_cast( catPtr.data() ); QString member = cat->nameForId( memberItem.toInt() ); if (member.isNull()) continue; m_db->m_members.addMemberToGroup( category, group, member ); } if(members.size() == 0) { // Groups are stored even if they are empty, so we also have to read them. // With no members, the above for loop will not be executed. m_db->m_members.addGroup(category, group); } } reader->readEndElement(); } } } /* void XMLDB::FileReader::loadSettings(ReaderPtr reader) { static QString settingsString = QString::fromUtf8("settings"); static QString settingString = QString::fromUtf8("setting"); static QString keyString = QString::fromUtf8("key"); static QString valueString = QString::fromUtf8("value"); ElementInfo info = reader->peekNext(); if (info.isStartToken && info.tokenName == settingsString) { reader->readNextStartOrStopElement(settingString); while(reader->readNextStartOrStopElement(settingString).isStartToken) { if (reader->hasAttribute(keyString) && reader->hasAttribute(valueString)) { m_db->m_settings.insert(unescape(reader->attribute(keyString)), unescape(reader->attribute(valueString))); } else { qWarning() << "File corruption in index.xml. Setting either lacking a key or a " << "value attribute. Ignoring this entry."; } reader->readEndElement(); } } } */ void XMLDB::FileReader::checkIfImagesAreSorted() { if ( !KMessageBox::shouldBeShownContinue( QString::fromLatin1( "checkWhetherImagesAreSorted" ) ) ) return; QDateTime last( QDate( 1900, 1, 1 ) ); bool wrongOrder = false; for( DB::ImageInfoListIterator it = m_db->m_images.begin(); !wrongOrder && it != m_db->m_images.end(); ++it ) { if ( last > (*it)->date().start() && (*it)->date().start().isValid() ) wrongOrder = true; last = (*it)->date().start(); } if ( wrongOrder ) { KMessageBox::information( messageParent(), i18n("

Your images/videos are not sorted, which means that navigating using the date bar " "will only work suboptimally.

" "

In the Maintenance menu, you can find Display Images with Incomplete Dates " "which you can use to find the images that are missing date information.

" "

You can then select the images that you have reason to believe have a correct date " "in either their Exif data or on the file, and execute Maintenance->Read Exif Info " "to reread the information.

" "

Finally, once all images have their dates set, you can execute " "Maintenance->Sort All by Date & Time to sort them in the database.

"), i18n("Images/Videos Are Not Sorted"), QString::fromLatin1( "checkWhetherImagesAreSorted" ) ); } } void XMLDB::FileReader::checkIfAllImagesHaveSizeAttributes() { QTime time; time.start(); if ( !KMessageBox::shouldBeShownContinue( QString::fromLatin1( "checkWhetherAllImagesIncludesSize" ) ) ) return; if ( m_db->s_anyImageWithEmptySize ) { KMessageBox::information( messageParent(), i18n("

Not all the images in the database have information about image sizes; this is needed to " "get the best result in the thumbnail view. To fix this, simply go to the Maintenance menu, " "and first choose Remove All Thumbnails, and after that choose Build Thumbnails.

" "

Not doing so will result in extra space around images in the thumbnail view - that is all - so " "there is no urgency in doing it.

"), i18n("Not All Images Have Size Information"), QString::fromLatin1( "checkWhetherAllImagesIncludesSize" ) ); } } DB::ImageInfoPtr XMLDB::FileReader::load( const DB::FileName& fileName, ReaderPtr reader ) { DB::ImageInfoPtr info = XMLDB::Database::createImageInfo(fileName, reader, m_db); m_nextStackId = qMax( m_nextStackId, info->stackId() + 1 ); info->createFolderCategoryItem( m_folderCategory, m_db->m_members ); return info; } XMLDB::ReaderPtr XMLDB::FileReader::readConfigFile( const QString& configFile ) { ReaderPtr reader = ReaderPtr(new XmlReader); QFile file( configFile ); if ( !file.exists() ) { // Load a default setup QFile file(Utilities::locateDataFile(QString::fromLatin1("default-setup"))); if ( !file.open( QIODevice::ReadOnly ) ) { KMessageBox::information( messageParent(), i18n( "

KPhotoAlbum was unable to load a default setup, which indicates an installation error

" "

If you have installed KPhotoAlbum yourself, then you must remember to set the environment variable " "KDEDIRS, to point to the topmost installation directory.

" "

If you for example ran cmake with -DCMAKE_INSTALL_PREFIX=/usr/local/kde, then you must use the following " "environment variable setup (this example is for Bash and compatible shells):

" "

export KDEDIRS=/usr/local/kde

" "

In case you already have KDEDIRS set, simply append the string as if you where setting the PATH " "environment variable

"), i18n("No default setup file found") ); } else { QTextStream stream( &file ); stream.setCodec( QTextCodec::codecForName("UTF-8") ); QString str = stream.readAll(); // Replace the default setup's category and tag names with localized ones str = str.replace(QString::fromUtf8("People"), i18n("People")); str = str.replace(QString::fromUtf8("Places"), i18n("Places")); str = str.replace(QString::fromUtf8("Events"), i18n("Events")); str = str.replace(QString::fromUtf8("untagged"), i18n("untagged")); str = str.replace( QRegExp( QString::fromLatin1("imageDirectory=\"[^\"]*\"")), QString::fromLatin1("") ); str = str.replace( QRegExp( QString::fromLatin1("htmlBaseDir=\"[^\"]*\"")), QString::fromLatin1("") ); str = str.replace( QRegExp( QString::fromLatin1("htmlBaseURL=\"[^\"]*\"")), QString::fromLatin1("") ); reader->addData(str); } } else { if ( !file.open( QIODevice::ReadOnly ) ) { KMessageBox::error( messageParent(), i18n("Unable to open '%1' for reading", configFile ), i18n("Error Running Demo") ); exit(-1); } reader->addData(file.readAll()); #if 0 QString errMsg; int errLine; int errCol; if ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol )) { file.close(); // If parsing index.xml fails let's see if we could use a backup instead Utilities::checkForBackupFile( configFile, i18n( "line %1 column %2 in file %3: %4", errLine , errCol , configFile , errMsg ) ); if ( !file.open( QIODevice::ReadOnly ) || ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol ) ) ) { KMessageBox::error( messageParent(), i18n( "Failed to recover the backup: %1", errMsg ) ); exit(-1); } } #endif } // Now read the content of the file. #if 0 QDomElement top = doc.documentElement(); if ( top.isNull() ) { KMessageBox::error( messageParent(), i18n("Error in file %1: No elements found", configFile ) ); exit(-1); } if ( top.tagName().toLower() != QString::fromLatin1( "kphotoalbum" ) && top.tagName().toLower() != QString::fromLatin1( "kimdaba" ) ) { // KimDaBa compatibility KMessageBox::error( messageParent(), i18n("Error in file %1: expected 'KPhotoAlbum' as top element but found '%2'", configFile , top.tagName() ) ); exit(-1); } #endif file.close(); return reader; } QString XMLDB::FileReader::unescape( const QString& str ) { static QHash cache; if ( cache.contains(str) ) return cache[str]; QString tmp( str ); // Matches encoded characters in attribute names QRegExp rx( QString::fromLatin1( "(_.)([0-9A-F]{2})" ) ); int pos = 0; // Unencoding special characters if compressed XML is selected if ( useCompressedFileFormat() ) { while ( ( pos = rx.indexIn( tmp, pos ) ) != -1 ) { QString before = rx.cap( 1 ) + rx.cap( 2 ); QString after = QString::fromLatin1( QByteArray::fromHex( rx.cap( 2 ).toLocal8Bit() ) ); tmp.replace( pos, before.length(), after ); pos += after.length(); } } else tmp.replace( QString::fromLatin1( "_" ), QString::fromLatin1( " " ) ); cache.insert(str,tmp); return tmp; } // TODO(hzeller): DEPENDENCY This pulls in the whole MainWindow dependency into the database backend. QWidget *XMLDB::FileReader::messageParent() { return MainWindow::Window::theMainWindow(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileReader.h b/XMLDB/FileReader.h index ffb52888..2614df67 100644 --- a/XMLDB/FileReader.h +++ b/XMLDB/FileReader.h @@ -1,74 +1,80 @@ /* 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 XMLDB_FILEREADER_H #define XMLDB_FILEREADER_H #include #include "DB/ImageInfoPtr.h" #include "DB/ImageInfo.h" #include #include "XmlReader.h" class QXmlStreamReader; namespace XMLDB { class Database; class FileReader { public: FileReader( Database* db ) : m_db( db ), m_nextStackId(1) {} void read( const QString& configFile ); static QString unescape( const QString& ); DB::StackID nextStackId() const { return m_nextStackId; }; + struct DuplicateInfo { + DB::ImageInfoPtr first; + DB::ImageInfoPtr duplicate; + }; + QList duplicates() const { return m_duplicateList; } protected: void loadCategories( ReaderPtr reader ); void loadImages( ReaderPtr reader ); void loadBlockList( ReaderPtr reader ); void loadMemberGroups( ReaderPtr reader ); //void loadSettings(ReaderPtr reader); DB::ImageInfoPtr load( const DB::FileName& filename, ReaderPtr reader ); ReaderPtr readConfigFile( const QString& configFile ); void createSpecialCategories(); void checkIfImagesAreSorted(); void checkIfAllImagesHaveSizeAttributes(); // The parent widget information dialogs are displayed in. QWidget *messageParent(); private: Database* const m_db; int m_fileVersion; DB::StackID m_nextStackId; + QList m_duplicateList; // During profilation I found that it was rather expensive to look this up over and over again (once for each image) DB::CategoryPtr m_folderCategory; }; } #endif /* XMLDB_FILEREADER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: