diff --git a/DB/ImageInfo.cpp b/DB/ImageInfo.cpp index 57721424..4197b91d 100644 --- a/DB/ImageInfo.cpp +++ b/DB/ImageInfo.cpp @@ -1,811 +1,812 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 "CategoryCollection.h" #include "FileInfo.h" #include "ImageDB.h" #include "Logging.h" #include "MemberMap.h" #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) { } 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) { 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; } ImageInfo::ImageInfo(const ImageInfo &other) : QSharedData(other) { *this = other; } 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; } void ImageInfo::setLabel(const QString &desc) { if (desc != m_label) m_dirty = true; m_label = desc; } QString ImageInfo::label() const { return m_label; } void ImageInfo::setDescription(const QString &desc) { if (desc != m_description) m_dirty = true; m_description = desc.trimmed(); } 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; } 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); } } 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 ); } } 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; } } } } int ImageInfo::angle() const { return m_angle; } void ImageInfo::setAngle(int angle) { if (angle != m_angle) m_dirty = true; m_angle = angle; } 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; } 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; } 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; } void ImageInfo::setVideoLength(int length) { if (m_videoLength != length) m_dirty = true; m_videoLength = length; } int ImageInfo::videoLength() const { return m_videoLength; } void ImageInfo::setDate(const ImageDate &date) { if (date != m_date) m_dirty = true; m_date = date; } 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); } 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); if (storeEXIF) { // Size isn't really EXIF, but this is the most obvious // place to extract it QImageReader reader(m_fileName.absolute()); if (reader.canRead()) { m_size = reader.size(); if (m_angle == 90 || m_angle == 270) m_size.transpose(); } } // 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; } 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); // 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); } } // 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; } 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_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; if (rating > 10) rating = 10; if (rating < -1) rating = -1; m_rating = rating; m_stackId = stackId; m_stackOrder = stackOrder; m_videoLength = -1; + m_matchGeneration = -1; } // Note: we need this operator because the base class QSharedData hides // its copy operator to make exclude the reference counting from being // copied. 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_rating = other.m_rating; m_stackId = other.m_stackId; m_stackOrder = other.m_stackOrder; m_videoLength = other.m_videoLength; m_isMatched = other.m_isMatched; m_matchGeneration = other.m_matchGeneration; #ifdef HAVE_KGEOMAP m_coordinates = other.m_coordinates; m_coordsIsSet = other.m_coordsIsSet; #endif m_locked = other.m_locked; m_dirty = other.m_dirty; 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); } } } 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); } } } 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; } } } 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); } } void DB::ImageInfo::setPositionedTags(const QString &category, const QMap &positionedTags) { m_dirty = true; m_taggedAreas[category] = positionedTags; } 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/ImageSearchInfo.cpp b/DB/ImageSearchInfo.cpp index e2680bc7..23bac64e 100644 --- a/DB/ImageSearchInfo.cpp +++ b/DB/ImageSearchInfo.cpp @@ -1,665 +1,667 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 "ImageSearchInfo.h" #include "AndCategoryMatcher.h" #include "CategoryMatcher.h" #include "ContainerCategoryMatcher.h" #include "ExactCategoryMatcher.h" #include "ImageDB.h" #include "Logging.h" #include "NegationCategoryMatcher.h" #include "NoTagCategoryMatcher.h" #include "OrCategoryMatcher.h" #include "ValueCategoryMatcher.h" #include #include #include #include #include #include #include using namespace DB; static QAtomicInt s_matchGeneration; static int nextGeneration() { return s_matchGeneration++; } ImageSearchInfo::ImageSearchInfo(const ImageDate &date, const QString &label, const QString &description) : m_date(date) , m_label(label) , m_description(description) , m_rating(-1) , m_megapixel(0) , m_max_megapixel(0) , m_ratingSearchMode(0) , m_searchRAW(false) , m_isNull(false) , m_isCacheable(true) , m_compiled(false) , m_matchGeneration(nextGeneration()) { } ImageSearchInfo::ImageSearchInfo(const ImageDate &date, const QString &label, const QString &description, const QString &fnPattern) : m_date(date) , m_label(label) , m_description(description) , m_fnPattern(fnPattern) , m_rating(-1) , m_megapixel(0) , m_max_megapixel(0) , m_ratingSearchMode(0) , m_searchRAW(false) , m_isNull(false) , m_isCacheable(true) , m_compiled(false) , m_matchGeneration(nextGeneration()) { } QString ImageSearchInfo::label() const { return m_label; } QRegExp ImageSearchInfo::fnPattern() const { return m_fnPattern; } QString ImageSearchInfo::description() const { return m_description; } void ImageSearchInfo::checkIfNull() { if (m_compiled || isNull()) return; if (m_date.isNull() && m_label.isEmpty() && m_description.isEmpty() && m_rating == -1 && m_megapixel == 0 && m_exifSearchInfo.isNull() && m_categoryMatchText.isEmpty() #ifdef HAVE_KGEOMAP && !m_regionSelection.first.hasCoordinates() && !m_regionSelection.second.hasCoordinates() #endif ) { m_isNull = true; } } ImageSearchInfo::ImageSearchInfo() : m_rating(-1) , m_megapixel(0) , m_max_megapixel(0) , m_ratingSearchMode(0) , m_searchRAW(false) , m_isNull(true) , m_isCacheable(true) , m_compiled(false) , m_matchGeneration(nextGeneration()) { } bool ImageSearchInfo::isNull() const { return m_isNull; } bool ImageSearchInfo::isCacheable() const { return m_isCacheable; } void ImageSearchInfo::setCacheable(bool cacheable) { m_isCacheable = cacheable; } bool ImageSearchInfo::match(ImageInfoPtr info) const { if (m_isNull) return true; if (m_isCacheable && info->matchGeneration() == m_matchGeneration) return info->isMatched(); bool ok = doMatch(info); if (m_isCacheable) { info->setMatchGeneration(m_matchGeneration); info->setIsMatched(ok); } return ok; } bool ImageSearchInfo::doMatch(ImageInfoPtr info) const { if (!m_compiled) compile(); // -------------------------------------------------- Rating //ok = ok && (_rating == -1 ) || ( _rating == info->rating() ); if (m_rating != -1) { switch (m_ratingSearchMode) { case 1: // Image rating at least selected if (m_rating > info->rating()) return false; break; case 2: // Image rating less than selected if (m_rating < info->rating()) return false; break; case 3: // Image rating not equal if (m_rating == info->rating()) return false; break; default: if (m_rating != info->rating()) return false; break; } } // -------------------------------------------------- Resolution if (m_megapixel && (m_megapixel * 1000000 > info->size().width() * info->size().height())) return false; if (m_max_megapixel && m_max_megapixel < m_megapixel && (m_max_megapixel * 1000000 < info->size().width() * info->size().height())) return false; // -------------------------------------------------- Date QDateTime actualStart = info->date().start(); QDateTime actualEnd = info->date().end(); if (m_date.start().isValid()) { if (actualEnd < m_date.start() || (m_date.end().isValid() && actualStart > m_date.end())) return false; } else if (m_date.end().isValid() && actualStart > m_date.end()) { return false; } // -------------------------------------------------- Label if (m_label.isEmpty() && info->label().indexOf(m_label) == -1) return false; // -------------------------------------------------- RAW if (m_searchRAW && !ImageManager::RAWImageDecoder::isRAW(info->fileName())) return false; #ifdef HAVE_KGEOMAP // Search for GPS Position if (m_usingRegionSelection) { if (!info->coordinates().hasCoordinates()) return false; float infoLat = info->coordinates().lat(); if (m_regionSelectionMinLat > infoLat || m_regionSelectionMaxLat < infoLat) return false; float infoLon = info->coordinates().lon(); if (m_regionSelectionMinLon > infoLon || m_regionSelectionMaxLon < infoLon) return false; } #endif // -------------------------------------------------- File name pattern if (!m_fnPattern.isEmpty() && m_fnPattern.indexIn(info->fileName().relative()) == -1) return false; // -------------------------------------------------- Options // alreadyMatched map is used to make it possible to search for // Jesper & None QMap alreadyMatched; for (CategoryMatcher *optionMatcher : m_categoryMatchers) { if (!optionMatcher->eval(info, alreadyMatched)) return false; } // -------------------------------------------------- Text if (!m_description.isEmpty()) { const QString &txt(info->description()); QStringList list = m_description.split(QChar::fromLatin1(' '), QString::SkipEmptyParts); Q_FOREACH (const QString &word, list) { if (txt.indexOf(word, 0, Qt::CaseInsensitive) == -1) return false; } } // -------------------------------------------------- EXIF if (!m_exifSearchInfo.matches(info->fileName())) return false; return true; } QString ImageSearchInfo::categoryMatchText(const QString &name) const { return m_categoryMatchText[name]; } void ImageSearchInfo::setCategoryMatchText(const QString &name, const QString &value) { if (value.isEmpty()) { m_categoryMatchText.remove(name); } else { m_categoryMatchText[name] = value; } m_isNull = false; m_compiled = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::addAnd(const QString &category, const QString &value) { // Escape literal "&"s in value by doubling it QString escapedValue = value; escapedValue.replace(QString::fromUtf8("&"), QString::fromUtf8("&&")); QString val = categoryMatchText(category); if (!val.isEmpty()) val += QString::fromLatin1(" & ") + escapedValue; else val = escapedValue; setCategoryMatchText(category, val); m_isNull = false; m_compiled = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setRating(short rating) { m_rating = rating; m_isNull = false; m_compiled = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setMegaPixel(short megapixel) { m_megapixel = megapixel; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setMaxMegaPixel(short max_megapixel) { m_max_megapixel = max_megapixel; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setSearchMode(int index) { m_ratingSearchMode = index; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setSearchRAW(bool searchRAW) { m_searchRAW = searchRAW; m_matchGeneration = nextGeneration(); } QString ImageSearchInfo::toString() const { QString res; bool first = true; for (QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { if (!it.value().isEmpty()) { if (first) first = false; else res += QString::fromLatin1(" / "); QString txt = it.value(); if (txt == ImageDB::NONE()) txt = i18nc("As in No persons, no locations etc. I do realize that translators may have problem with this, " "but I need some how to indicate the category, and users may create their own categories, so this is " "the best I can do - Jesper.", "No %1", it.key()); if (txt.contains(QString::fromLatin1("|"))) txt.replace(QString::fromLatin1("&"), QString::fromLatin1(" %1 ").arg(i18n("and"))); else txt.replace(QString::fromLatin1("&"), QString::fromLatin1(" / ")); txt.replace(QString::fromLatin1("|"), QString::fromLatin1(" %1 ").arg(i18n("or"))); txt.replace(QString::fromLatin1("!"), QString::fromLatin1(" %1 ").arg(i18n("not"))); txt.replace(ImageDB::NONE(), i18nc("As in no other persons, or no other locations. " "I do realize that translators may have problem with this, " "but I need some how to indicate the category, and users may create their own categories, so this is " "the best I can do - Jesper.", "No other %1", it.key())); res += txt.simplified(); } } return res; } void ImageSearchInfo::debug() { for (QMap::Iterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { qCDebug(DBCategoryMatcherLog) << it.key() << ", " << it.value(); } } // PENDING(blackie) move this into the Options class instead of having it here. void ImageSearchInfo::saveLock() const { KConfigGroup config = KSharedConfig::openConfig()->group(Settings::SettingsData::instance()->groupForDatabase("Privacy Settings")); config.writeEntry(QString::fromLatin1("label"), m_label); config.writeEntry(QString::fromLatin1("description"), m_description); config.writeEntry(QString::fromLatin1("categories"), m_categoryMatchText.keys()); for (QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { config.writeEntry(it.key(), it.value()); } config.sync(); } ImageSearchInfo ImageSearchInfo::loadLock() { KConfigGroup config = KSharedConfig::openConfig()->group(Settings::SettingsData::instance()->groupForDatabase("Privacy Settings")); ImageSearchInfo info; info.m_label = config.readEntry("label"); info.m_description = config.readEntry("description"); QStringList categories = config.readEntry(QString::fromLatin1("categories"), QStringList()); for (QStringList::ConstIterator it = categories.constBegin(); it != categories.constEnd(); ++it) { info.setCategoryMatchText(*it, config.readEntry(*it, QString())); } return info; } ImageSearchInfo::ImageSearchInfo(const ImageSearchInfo &other) { m_date = other.m_date; m_categoryMatchText = other.m_categoryMatchText; m_label = other.m_label; m_description = other.m_description; m_fnPattern = other.m_fnPattern; m_isNull = other.m_isNull; m_compiled = false; m_rating = other.m_rating; m_ratingSearchMode = other.m_ratingSearchMode; m_megapixel = other.m_megapixel; m_max_megapixel = other.m_max_megapixel; m_searchRAW = other.m_searchRAW; m_exifSearchInfo = other.m_exifSearchInfo; m_matchGeneration = other.m_matchGeneration; m_isCacheable = other.m_isCacheable; #ifdef HAVE_KGEOMAP m_regionSelection = other.m_regionSelection; #endif } void ImageSearchInfo::compile() const { m_exifSearchInfo.search(); #ifdef HAVE_KGEOMAP // Prepare Search for GPS Position m_usingRegionSelection = m_regionSelection.first.hasCoordinates() && m_regionSelection.second.hasCoordinates(); if (m_usingRegionSelection) { using std::max; using std::min; m_regionSelectionMinLat = min(m_regionSelection.first.lat(), m_regionSelection.second.lat()); m_regionSelectionMaxLat = max(m_regionSelection.first.lat(), m_regionSelection.second.lat()); m_regionSelectionMinLon = min(m_regionSelection.first.lon(), m_regionSelection.second.lon()); m_regionSelectionMaxLon = max(m_regionSelection.first.lon(), m_regionSelection.second.lon()); } #endif deleteMatchers(); for (QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { QString category = it.key(); QString matchText = it.value(); QStringList orParts = matchText.split(QString::fromLatin1("|"), QString::SkipEmptyParts); DB::ContainerCategoryMatcher *orMatcher = new DB::OrCategoryMatcher; Q_FOREACH (QString orPart, orParts) { // Split by " & ", not only by "&", so that the doubled "&"s won't be used as a split point QStringList andParts = orPart.split(QString::fromLatin1(" & "), QString::SkipEmptyParts); DB::ContainerCategoryMatcher *andMatcher; bool exactMatch = false; bool negate = false; andMatcher = new DB::AndCategoryMatcher; Q_FOREACH (QString str, andParts) { static QRegExp regexp(QString::fromLatin1("^\\s*!\\s*(.*)$")); if (regexp.exactMatch(str)) { // str is preceded with NOT negate = true; str = regexp.cap(1); } str = str.trimmed(); CategoryMatcher *valueMatcher; if (str == ImageDB::NONE()) { // mark AND-group as containing a "No other" condition exactMatch = true; continue; } else { valueMatcher = new DB::ValueCategoryMatcher(category, str); if (negate) valueMatcher = new DB::NegationCategoryMatcher(valueMatcher); } andMatcher->addElement(valueMatcher); } if (exactMatch) { DB::CategoryMatcher *exactMatcher = nullptr; // if andMatcher has exactMatch set, but no CategoryMatchers, then // matching "category / None" is what we want: if (andMatcher->mp_elements.count() == 0) { exactMatcher = new DB::NoTagCategoryMatcher(category); } else { ExactCategoryMatcher *noOtherMatcher = new ExactCategoryMatcher(category); if (andMatcher->mp_elements.count() == 1) noOtherMatcher->setMatcher(andMatcher->mp_elements[0]); else noOtherMatcher->setMatcher(andMatcher); exactMatcher = noOtherMatcher; } if (negate) exactMatcher = new DB::NegationCategoryMatcher(exactMatcher); orMatcher->addElement(exactMatcher); } else if (andMatcher->mp_elements.count() == 1) orMatcher->addElement(andMatcher->mp_elements[0]); else if (andMatcher->mp_elements.count() > 1) orMatcher->addElement(andMatcher); } CategoryMatcher *matcher = nullptr; if (orMatcher->mp_elements.count() == 1) matcher = orMatcher->mp_elements[0]; else if (orMatcher->mp_elements.count() > 1) matcher = orMatcher; if (matcher) { m_categoryMatchers.append(matcher); if (DBCategoryMatcherLog().isDebugEnabled()) { qCDebug(DBCategoryMatcherLog) << "Matching text '" << matchText << "' in category " << category << ":"; matcher->debug(0); qCDebug(DBCategoryMatcherLog) << "."; } } } m_compiled = true; } ImageSearchInfo::~ImageSearchInfo() { deleteMatchers(); } void ImageSearchInfo::debugMatcher() const { if (!m_compiled) compile(); qCDebug(DBCategoryMatcherLog, "And:"); for (CategoryMatcher *optionMatcher : m_categoryMatchers) { optionMatcher->debug(1); } } QList> ImageSearchInfo::query() const { if (!m_compiled) compile(); // Combine _optionMachers to one list of lists in Disjunctive // Normal Form and return it. QList::Iterator it = m_categoryMatchers.begin(); QList> result; if (it == m_categoryMatchers.end()) return result; result = convertMatcher(*it); ++it; for (; it != m_categoryMatchers.end(); ++it) { QList> current = convertMatcher(*it); QList> oldResult = result; result.clear(); for (QList resultIt : oldResult) { for (QList currentIt : current) { QList tmp; tmp += resultIt; tmp += currentIt; result.append(tmp); } } } return result; } Utilities::StringSet ImageSearchInfo::findAlreadyMatched(const QString &group) const { Utilities::StringSet result; QString str = categoryMatchText(group); if (str.contains(QString::fromLatin1("|"))) { return result; } QStringList list = str.split(QString::fromLatin1("&"), QString::SkipEmptyParts); Q_FOREACH (QString part, list) { QString nm = part.trimmed(); if (!nm.contains(QString::fromLatin1("!"))) result.insert(nm); } return result; } void ImageSearchInfo::deleteMatchers() const { qDeleteAll(m_categoryMatchers); m_categoryMatchers.clear(); } QList ImageSearchInfo::extractAndMatcher(CategoryMatcher *matcher) const { QList result; AndCategoryMatcher *andMatcher; SimpleCategoryMatcher *simpleMatcher; if ((andMatcher = dynamic_cast(matcher))) { for (CategoryMatcher *child : andMatcher->mp_elements) { SimpleCategoryMatcher *simpleMatcher = dynamic_cast(child); Q_ASSERT(simpleMatcher); result.append(simpleMatcher); } } else if ((simpleMatcher = dynamic_cast(matcher))) result.append(simpleMatcher); else Q_ASSERT(false); return result; } /** Convert matcher to Disjunctive Normal Form. * * @return OR-list of AND-lists. (e.g. OR(AND(a,b),AND(c,d))) */ QList> ImageSearchInfo::convertMatcher(CategoryMatcher *item) const { QList> result; OrCategoryMatcher *orMacther; if ((orMacther = dynamic_cast(item))) { for (CategoryMatcher *child : orMacther->mp_elements) { result.append(extractAndMatcher(child)); } } else result.append(extractAndMatcher(item)); return result; } short ImageSearchInfo::rating() const { return m_rating; } ImageDate ImageSearchInfo::date() const { return m_date; } void ImageSearchInfo::addExifSearchInfo(const Exif::SearchInfo info) { m_exifSearchInfo = info; m_isNull = false; + m_matchGeneration = nextGeneration(); } void DB::ImageSearchInfo::renameCategory(const QString &oldName, const QString &newName) { m_categoryMatchText[newName] = m_categoryMatchText[oldName]; m_categoryMatchText.remove(oldName); m_compiled = false; + m_matchGeneration = nextGeneration(); } #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates::Pair ImageSearchInfo::regionSelection() const { return m_regionSelection; } void ImageSearchInfo::setRegionSelection(const KGeoMap::GeoCoordinates::Pair &actRegionSelection) { m_regionSelection = actRegionSelection; m_compiled = false; if (m_regionSelection.first.hasCoordinates() && m_regionSelection.second.hasCoordinates()) { m_isNull = false; } m_matchGeneration = nextGeneration(); } #endif // vi:expandtab:tabstop=4 shiftwidth=4: