File indexing completed on 2024-05-12 15:55:22

0001 // SPDX-FileCopyrightText: 2003-2013 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0002 // SPDX-FileCopyrightText: 2005-2007 Dirk Mueller <mueller@kde.org>
0003 // SPDX-FileCopyrightText: 2006-2008 Tuomas Suutari <tuomas@nepnep.net>
0004 // SPDX-FileCopyrightText: 2007-2010 Jan Kundrát <jkt@flaska.net>
0005 // SPDX-FileCopyrightText: 2007-2008 Laurent Montel <montel@kde.org>
0006 // SPDX-FileCopyrightText: 2008 Henner Zeller <h.zeller@acm.org>
0007 // SPDX-FileCopyrightText: 2009 Hassan Ibraheem <hasan.ibraheem@gmail.com>
0008 // SPDX-FileCopyrightText: 2011-2012 Miika Turkia <miika.turkia@gmail.com>
0009 // SPDX-FileCopyrightText: 2012-2016, 2018-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0010 // SPDX-FileCopyrightText: 2014-2020 Tobias Leupold <tl@stonemx.de>
0011 // SPDX-FileCopyrightText: 2017-2020 Robert Krawitz <rlk@alum.mit.edu>
0012 //
0013 // SPDX-License-Identifier: GPL-2.0-or-later
0014 
0015 #include "ImageSearchInfo.h"
0016 
0017 #include "AndCategoryMatcher.h"
0018 #include "CategoryMatcher.h"
0019 #include "ContainerCategoryMatcher.h"
0020 #include "ExactCategoryMatcher.h"
0021 #include "NegationCategoryMatcher.h"
0022 #include "NoTagCategoryMatcher.h"
0023 #include "OrCategoryMatcher.h"
0024 #include "ValueCategoryMatcher.h"
0025 #include "WildcardCategoryMatcher.h"
0026 
0027 #include <DB/ImageDB.h>
0028 #include <ImageManager/RawImageDecoder.h>
0029 #include <kpabase/FileExtensions.h>
0030 #include <kpabase/Logging.h>
0031 #include <kpabase/SettingsData.h>
0032 
0033 #include <KConfigGroup>
0034 #include <KLocalizedString>
0035 #include <KSharedConfig>
0036 #include <QApplication>
0037 #include <QRegExp>
0038 #include <QRegularExpression>
0039 
0040 using namespace DB;
0041 
0042 static QAtomicInt s_matchGeneration;
0043 static int nextGeneration()
0044 {
0045     return s_matchGeneration++;
0046 }
0047 
0048 ImageSearchInfo::ImageSearchInfo()
0049     : m_matchGeneration(nextGeneration())
0050 {
0051 }
0052 
0053 ImageSearchInfo::ImageSearchInfo(const ImageDate &date,
0054                                  const QString &label, const QString &description)
0055     : m_date(date)
0056     , m_label(label)
0057     , m_description(description)
0058     , m_isNull(false)
0059     , m_matchGeneration(nextGeneration())
0060 {
0061 }
0062 
0063 ImageSearchInfo::ImageSearchInfo(const ImageDate &date,
0064                                  const QString &label, const QString &description,
0065                                  const QString &fnPattern)
0066     : m_date(date)
0067     , m_label(label)
0068     , m_description(description)
0069     , m_fnPattern(fnPattern)
0070     , m_isNull(false)
0071     , m_matchGeneration(nextGeneration())
0072 {
0073 }
0074 
0075 QString ImageSearchInfo::label() const
0076 {
0077     return m_label;
0078 }
0079 
0080 QRegExp ImageSearchInfo::fnPattern() const
0081 {
0082     return m_fnPattern;
0083 }
0084 
0085 QString ImageSearchInfo::description() const
0086 {
0087     return m_description;
0088 }
0089 
0090 void ImageSearchInfo::checkIfNull()
0091 {
0092     if (m_compiled.valid || isNull())
0093         return;
0094     if (m_date.isNull() && m_label.isEmpty() && m_description.isEmpty()
0095         && m_rating == -1 && m_megapixel == 0 && m_exifSearchInfo.isEmpty()
0096         && m_categoryMatchText.isEmpty()
0097         && freeformMatchText().isEmpty()
0098 #ifdef HAVE_KGEOMAP
0099         && !m_regionSelection.first.hasCoordinates() && !m_regionSelection.second.hasCoordinates()
0100 #endif
0101     ) {
0102         m_isNull = true;
0103     }
0104 }
0105 
0106 bool ImageSearchInfo::isNull() const
0107 {
0108     return m_isNull;
0109 }
0110 
0111 bool ImageSearchInfo::isCacheable() const
0112 {
0113     return m_isCacheable;
0114 }
0115 
0116 void ImageSearchInfo::setCacheable(bool cacheable)
0117 {
0118     m_isCacheable = cacheable;
0119 }
0120 
0121 bool ImageSearchInfo::match(ImageInfoPtr info) const
0122 {
0123     if (m_isNull)
0124         return true;
0125 
0126     if (m_isCacheable && info->matchGeneration() == m_matchGeneration)
0127         return info->isMatched();
0128 
0129     bool ok = doMatch(info);
0130 
0131     if (m_isCacheable) {
0132         info->setMatchGeneration(m_matchGeneration);
0133         info->setIsMatched(ok);
0134     }
0135     return ok;
0136 }
0137 
0138 bool ImageSearchInfo::doMatch(ImageInfoPtr info) const
0139 {
0140     if (!m_compiled.valid)
0141         compile();
0142 
0143     // -------------------------------------------------- Rating
0144 
0145     // ok = ok && (_rating == -1 ) || ( _rating == info->rating() );
0146     if (m_rating != -1) {
0147         switch (m_ratingSearchMode) {
0148         case 1:
0149             // Image rating at least selected
0150             if (m_rating > info->rating())
0151                 return false;
0152             break;
0153         case 2:
0154             // Image rating less than selected
0155             if (m_rating < info->rating())
0156                 return false;
0157             break;
0158         case 3:
0159             // Image rating not equal
0160             if (m_rating == info->rating())
0161                 return false;
0162             break;
0163         default:
0164             if (m_rating != info->rating())
0165                 return false;
0166             break;
0167         }
0168     }
0169 
0170     // -------------------------------------------------- Resolution
0171     if (m_megapixel && (m_megapixel * 1000000 > info->size().width() * info->size().height()))
0172         return false;
0173 
0174     if (m_max_megapixel && m_max_megapixel < m_megapixel && (m_max_megapixel * 1000000 < info->size().width() * info->size().height()))
0175         return false;
0176 
0177     // -------------------------------------------------- Date
0178     Utilities::FastDateTime actualStart = info->date().start();
0179     Utilities::FastDateTime actualEnd = info->date().end();
0180 
0181     if (m_date.start().isValid()) {
0182         if (actualEnd < m_date.start() || (m_date.end().isValid() && actualStart > m_date.end()))
0183             return false;
0184     } else if (m_date.end().isValid() && actualStart > m_date.end()) {
0185         return false;
0186     }
0187 
0188     // -------------------------------------------------- Label
0189     if (!m_label.isEmpty() && info->label().indexOf(m_label) == -1)
0190         return false;
0191 
0192     // -------------------------------------------------- RAW
0193     if (m_searchRAW && !KPABase::isUsableRawImage(info->fileName()))
0194         return false;
0195 
0196 #ifdef HAVE_MARBLE
0197     // Search for GPS Position
0198     if (!m_regionSelection.isNull()) {
0199         if (!info->coordinates().hasCoordinates())
0200             return false;
0201 
0202         if (!m_regionSelection.contains(info->coordinates()))
0203             return false;
0204     }
0205 #endif
0206 
0207     // -------------------------------------------------- File name pattern
0208     if (!m_fnPattern.isEmpty() && m_fnPattern.indexIn(info->fileName().relative()) == -1)
0209         return false;
0210 
0211     // -------------------------------------------------- Options
0212     // alreadyMatched map is used to make it possible to search for
0213     // Jesper & None
0214     QMap<QString, StringSet> alreadyMatched;
0215     for (CategoryMatcher *optionMatcher : m_compiled.categoryMatchers) {
0216         if (!optionMatcher->eval(info, alreadyMatched))
0217             return false;
0218     }
0219 
0220     // -------------------------------------------------- Text
0221     if (!m_description.isEmpty()) {
0222         const QString &txt(info->description());
0223         const QStringList list = m_description.split(QChar::fromLatin1(' '), Qt::SkipEmptyParts);
0224         for (const QString &word : list) {
0225             if (txt.indexOf(word, 0, Qt::CaseInsensitive) == -1)
0226                 return false;
0227         }
0228     }
0229 
0230     // -------------------------------------------------- EXIF
0231     if (!m_exifSearchInfo.matches(info->fileName()))
0232         return false;
0233 
0234     // -------------------------------------------------- Freeform
0235     if (!freeformMatchText().isEmpty()) {
0236         const auto re = m_freeformMatcher.regularExpression();
0237         if (!re.match(info->label()).hasMatch()
0238             && !re.match(info->fileName().relative()).hasMatch()
0239             && !re.match(info->description()).hasMatch()
0240             && !m_freeformMatcher.eval(info)) {
0241             return false;
0242         }
0243     }
0244 
0245     return true;
0246 }
0247 
0248 QString ImageSearchInfo::categoryMatchText(const QString &name) const
0249 {
0250     return m_categoryMatchText[name];
0251 }
0252 
0253 void ImageSearchInfo::setCategoryMatchText(const QString &name, const QString &value)
0254 {
0255     if (value.isEmpty()) {
0256         m_categoryMatchText.remove(name);
0257     } else {
0258         m_categoryMatchText[name] = value;
0259     }
0260     m_isNull = false;
0261     m_compiled.valid = false;
0262     m_matchGeneration = nextGeneration();
0263 }
0264 
0265 void ImageSearchInfo::addAnd(const QString &category, const QString &value)
0266 {
0267     // Escape literal "&"s in value by doubling it
0268     QString escapedValue = value;
0269     escapedValue.replace(QString::fromUtf8("&"), QString::fromUtf8("&&"));
0270 
0271     QString val = categoryMatchText(category);
0272     if (!val.isEmpty())
0273         val += QString::fromLatin1(" & ") + escapedValue;
0274     else
0275         val = escapedValue;
0276 
0277     setCategoryMatchText(category, val);
0278     m_isNull = false;
0279     m_compiled.valid = false;
0280     m_matchGeneration = nextGeneration();
0281 }
0282 
0283 void ImageSearchInfo::setRating(short rating)
0284 {
0285     m_rating = rating;
0286     m_isNull = false;
0287     m_matchGeneration = nextGeneration();
0288     // compiled data is not affected
0289 }
0290 
0291 void ImageSearchInfo::setMegaPixel(short megapixel)
0292 {
0293     m_megapixel = megapixel;
0294     m_matchGeneration = nextGeneration();
0295 }
0296 
0297 void ImageSearchInfo::setMaxMegaPixel(short max_megapixel)
0298 {
0299     m_max_megapixel = max_megapixel;
0300     m_matchGeneration = nextGeneration();
0301 }
0302 
0303 void ImageSearchInfo::setSearchMode(int index)
0304 {
0305     m_ratingSearchMode = index;
0306     m_matchGeneration = nextGeneration();
0307 }
0308 
0309 void ImageSearchInfo::setSearchRAW(bool searchRAW)
0310 {
0311     m_searchRAW = searchRAW;
0312     m_matchGeneration = nextGeneration();
0313 }
0314 
0315 QString ImageSearchInfo::toString() const
0316 {
0317     QString res;
0318     bool first = true;
0319     for (QMap<QString, QString>::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) {
0320         if (!it.value().isEmpty()) {
0321             if (first)
0322                 first = false;
0323             else
0324                 res += QString::fromLatin1(" / ");
0325 
0326             QString txt = it.value();
0327             if (txt == ImageDB::NONE())
0328                 txt = i18nc("As in No persons, no locations etc. I do realize that translators may have problem with this, "
0329                             "but I need some how to indicate the category, and users may create their own categories, so this is "
0330                             "the best I can do - Jesper.",
0331                             "No %1", it.key());
0332 
0333             if (txt.contains(QString::fromLatin1("|")))
0334                 txt.replace(QString::fromLatin1("&"), QString::fromLatin1(" %1 ").arg(i18n("and")));
0335 
0336             else
0337                 txt.replace(QString::fromLatin1("&"), QString::fromLatin1(" / "));
0338 
0339             txt.replace(QString::fromLatin1("|"), QString::fromLatin1(" %1 ").arg(i18n("or")));
0340             txt.replace(QString::fromLatin1("!"), QString::fromLatin1(" %1 ").arg(i18n("not")));
0341             txt.replace(ImageDB::NONE(), i18nc("As in no other persons, or no other locations. "
0342                                                "I do realize that translators may have problem with this, "
0343                                                "but I need some how to indicate the category, and users may create their own categories, so this is "
0344                                                "the best I can do - Jesper.",
0345                                                "No other %1", it.key()));
0346 
0347             res += txt.simplified();
0348         }
0349     }
0350     return res;
0351 }
0352 
0353 void ImageSearchInfo::debug()
0354 {
0355     for (QMap<QString, QString>::Iterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) {
0356         qCDebug(DBCategoryMatcherLog) << it.key() << ", " << it.value();
0357     }
0358 }
0359 
0360 QVariantMap ImageSearchInfo::getLockData() const
0361 {
0362     QVariantMap pairs;
0363     pairs[QString::fromLatin1("label")] = m_label;
0364     pairs[QString::fromLatin1("description")] = m_description;
0365     pairs[QString::fromLatin1("categories")] = QVariant(m_categoryMatchText.keys());
0366     for (QMap<QString, QString>::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) {
0367         pairs[it.key()] = it.value();
0368     }
0369     return pairs;
0370 }
0371 
0372 ImageSearchInfo ImageSearchInfo::loadLock(const QMap<QString, QVariant> &keyValuePairs)
0373 {
0374     ImageSearchInfo info;
0375     info.m_label = keyValuePairs.value(QString::fromLatin1("label"), {}).toString();
0376     info.m_description = keyValuePairs.value(QString::fromLatin1("description"), {}).toString();
0377     QStringList categories = keyValuePairs.value(QString::fromLatin1("categories"), {}).toStringList();
0378     for (QStringList::ConstIterator it = categories.constBegin(); it != categories.constEnd(); ++it) {
0379         info.setCategoryMatchText(*it, keyValuePairs.value(*it, {}).toString());
0380     }
0381     return info;
0382 }
0383 
0384 void ImageSearchInfo::compile() const
0385 {
0386     qCDebug(DBCategoryMatcherLog) << "Compiling search info...";
0387     m_exifSearchInfo.search();
0388 
0389     CompiledDataPrivate compiledData;
0390 
0391     for (QMap<QString, QString>::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) {
0392         const QString category = it.key();
0393         const QString matchText = it.value();
0394 
0395         const QStringList orParts = matchText.split(QString::fromLatin1("|"), Qt::SkipEmptyParts);
0396         DB::ContainerCategoryMatcher *orMatcher = new DB::OrCategoryMatcher;
0397 
0398         for (QString orPart : orParts) {
0399             // Split by " & ", not only by "&", so that the doubled "&"s won't be used as a split point
0400             const QStringList andParts = orPart.split(QString::fromLatin1(" & "), Qt::SkipEmptyParts);
0401 
0402             DB::ContainerCategoryMatcher *andMatcher;
0403             bool exactMatch = false;
0404             bool negate = false;
0405             andMatcher = new DB::AndCategoryMatcher;
0406 
0407             for (QString str : andParts) {
0408                 static const QRegExp regexp(QString::fromLatin1("^\\s*!\\s*(.*)$"));
0409                 if (regexp.exactMatch(str)) { // str is preceded with NOT
0410                     negate = true;
0411                     str = regexp.cap(1);
0412                 }
0413                 str = str.trimmed();
0414                 CategoryMatcher *valueMatcher;
0415                 if (str == ImageDB::NONE()) { // mark AND-group as containing a "No other" condition
0416                     exactMatch = true;
0417                     continue;
0418                 } else {
0419                     valueMatcher = new DB::ValueCategoryMatcher(category, str);
0420                     if (negate) {
0421                         valueMatcher = new DB::NegationCategoryMatcher(valueMatcher);
0422                         negate = false;
0423                     }
0424                 }
0425                 andMatcher->addElement(valueMatcher);
0426             }
0427             if (exactMatch) {
0428                 DB::CategoryMatcher *exactMatcher = nullptr;
0429                 // if andMatcher has exactMatch set, but no CategoryMatchers, then
0430                 // matching "category / None" is what we want:
0431                 if (andMatcher->mp_elements.count() == 0) {
0432                     delete andMatcher;
0433                     exactMatcher = new DB::NoTagCategoryMatcher(category);
0434                 } else {
0435                     ExactCategoryMatcher *noOtherMatcher = new ExactCategoryMatcher(category);
0436                     if (andMatcher->mp_elements.count() == 1)
0437                         noOtherMatcher->setMatcher(andMatcher->mp_elements[0]);
0438                     else
0439                         noOtherMatcher->setMatcher(andMatcher);
0440                     exactMatcher = noOtherMatcher;
0441                 }
0442                 if (negate)
0443                     exactMatcher = new DB::NegationCategoryMatcher(exactMatcher);
0444                 orMatcher->addElement(exactMatcher);
0445             } else if (andMatcher->mp_elements.count() == 1)
0446                 orMatcher->addElement(andMatcher->mp_elements[0]);
0447             else if (andMatcher->mp_elements.count() > 1)
0448                 orMatcher->addElement(andMatcher);
0449             else
0450                 delete andMatcher;
0451         }
0452         CategoryMatcher *matcher = nullptr;
0453         if (orMatcher->mp_elements.count() == 1)
0454             matcher = orMatcher->mp_elements[0];
0455         else if (orMatcher->mp_elements.count() > 1)
0456             matcher = orMatcher;
0457         else
0458             delete orMatcher;
0459 
0460         if (matcher) {
0461             compiledData.categoryMatchers.append(matcher);
0462             if (DBCategoryMatcherLog().isDebugEnabled()) {
0463                 qCDebug(DBCategoryMatcherLog) << "Matching text '" << matchText << "' in category " << category << ":";
0464                 matcher->debug(0);
0465                 qCDebug(DBCategoryMatcherLog) << ".";
0466             }
0467         }
0468     }
0469     compiledData.valid = true;
0470     std::swap(m_compiled, compiledData);
0471 }
0472 
0473 void ImageSearchInfo::debugMatcher() const
0474 {
0475     if (!m_compiled.valid)
0476         compile();
0477 
0478     qCDebug(DBCategoryMatcherLog, "And:");
0479     for (CategoryMatcher *optionMatcher : m_compiled.categoryMatchers) {
0480         optionMatcher->debug(1);
0481     }
0482 }
0483 
0484 QList<QList<SimpleCategoryMatcher *>> ImageSearchInfo::query() const
0485 {
0486     if (!m_compiled.valid)
0487         compile();
0488 
0489     // Combine _optionMachers to one list of lists in Disjunctive
0490     // Normal Form and return it.
0491 
0492     QList<CategoryMatcher *>::Iterator it = m_compiled.categoryMatchers.begin();
0493     QList<QList<SimpleCategoryMatcher *>> result;
0494     if (it == m_compiled.categoryMatchers.end())
0495         return result;
0496 
0497     result = convertMatcher(*it);
0498     ++it;
0499 
0500     for (; it != m_compiled.categoryMatchers.end(); ++it) {
0501         QList<QList<SimpleCategoryMatcher *>> current = convertMatcher(*it);
0502         QList<QList<SimpleCategoryMatcher *>> oldResult = result;
0503         result.clear();
0504 
0505         for (QList<SimpleCategoryMatcher *> resultIt : oldResult) {
0506             for (QList<SimpleCategoryMatcher *> currentIt : current) {
0507                 QList<SimpleCategoryMatcher *> tmp;
0508                 tmp += resultIt;
0509                 tmp += currentIt;
0510                 result.append(tmp);
0511             }
0512         }
0513     }
0514     return result;
0515 }
0516 
0517 Utilities::StringSet ImageSearchInfo::findAlreadyMatched(const QString &group) const
0518 {
0519     Utilities::StringSet result;
0520     const QString str = categoryMatchText(group);
0521     if (str.contains(QString::fromLatin1("|"))) {
0522         return result;
0523     }
0524 
0525     const QStringList list = str.split(QString::fromLatin1("&"), Qt::SkipEmptyParts);
0526     for (const QString &part : list) {
0527         const QString nm = part.trimmed();
0528         if (!nm.contains(QString::fromLatin1("!")))
0529             result.insert(nm);
0530     }
0531     return result;
0532 }
0533 
0534 QList<SimpleCategoryMatcher *> ImageSearchInfo::extractAndMatcher(CategoryMatcher *matcher) const
0535 {
0536     QList<SimpleCategoryMatcher *> result;
0537 
0538     AndCategoryMatcher *andMatcher;
0539     SimpleCategoryMatcher *simpleMatcher;
0540 
0541     if ((andMatcher = dynamic_cast<AndCategoryMatcher *>(matcher))) {
0542         for (CategoryMatcher *child : andMatcher->mp_elements) {
0543             SimpleCategoryMatcher *simpleMatcher = dynamic_cast<SimpleCategoryMatcher *>(child);
0544             Q_ASSERT(simpleMatcher);
0545             result.append(simpleMatcher);
0546         }
0547     } else if ((simpleMatcher = dynamic_cast<SimpleCategoryMatcher *>(matcher)))
0548         result.append(simpleMatcher);
0549     else
0550         Q_ASSERT(false);
0551 
0552     return result;
0553 }
0554 
0555 /** Convert matcher to Disjunctive Normal Form.
0556  *
0557  * @return OR-list of AND-lists. (e.g. OR(AND(a,b),AND(c,d)))
0558  */
0559 QList<QList<SimpleCategoryMatcher *>> ImageSearchInfo::convertMatcher(CategoryMatcher *item) const
0560 {
0561     QList<QList<SimpleCategoryMatcher *>> result;
0562     OrCategoryMatcher *orMatcher;
0563 
0564     if ((orMatcher = dynamic_cast<OrCategoryMatcher *>(item))) {
0565         for (CategoryMatcher *child : orMatcher->mp_elements) {
0566             result.append(extractAndMatcher(child));
0567         }
0568     } else
0569         result.append(extractAndMatcher(item));
0570     return result;
0571 }
0572 
0573 QString ImageSearchInfo::freeformMatchText() const
0574 {
0575     return m_freeformMatcher.regularExpression().pattern();
0576 }
0577 
0578 void ImageSearchInfo::setFreeformMatchText(const QString &freeformMatchText)
0579 {
0580     setCacheable(false);
0581     QRegularExpression re { freeformMatchText, QRegularExpression::CaseInsensitiveOption };
0582     m_freeformMatcher.setRegularExpression(re);
0583     m_isNull = m_isNull && freeformMatchText.isEmpty();
0584 }
0585 
0586 short ImageSearchInfo::rating() const
0587 {
0588     return m_rating;
0589 }
0590 
0591 ImageDate ImageSearchInfo::date() const
0592 {
0593     return m_date;
0594 }
0595 
0596 void ImageSearchInfo::addExifSearchInfo(const Exif::SearchInfo info)
0597 {
0598     m_exifSearchInfo = info;
0599     m_isNull = false;
0600     m_matchGeneration = nextGeneration();
0601 }
0602 
0603 void DB::ImageSearchInfo::renameCategory(const QString &oldName, const QString &newName)
0604 {
0605     m_categoryMatchText[newName] = m_categoryMatchText[oldName];
0606     m_categoryMatchText.remove(oldName);
0607     m_compiled.valid = false;
0608     m_matchGeneration = nextGeneration();
0609 }
0610 
0611 #ifdef HAVE_MARBLE
0612 Map::GeoCoordinates::LatLonBox ImageSearchInfo::regionSelection() const
0613 {
0614     return m_regionSelection;
0615 }
0616 
0617 void ImageSearchInfo::setRegionSelection(const Map::GeoCoordinates::LatLonBox &actRegionSelection)
0618 {
0619     m_regionSelection = actRegionSelection;
0620     if (!m_regionSelection.isNull()) {
0621         m_isNull = false;
0622     }
0623     m_matchGeneration = nextGeneration();
0624     // compiled data is not affected
0625 }
0626 #endif
0627 
0628 ImageSearchInfo::CompiledDataPrivate::CompiledDataPrivate(const ImageSearchInfo::CompiledDataPrivate &)
0629 {
0630     // copying invalidates the compiled data
0631     valid = false;
0632 }
0633 
0634 ImageSearchInfo::CompiledDataPrivate::~CompiledDataPrivate()
0635 {
0636     qDeleteAll(categoryMatchers);
0637     categoryMatchers.clear();
0638 }
0639 
0640 ImageSearchInfo::CompiledDataPrivate &ImageSearchInfo::CompiledDataPrivate::operator=(const ImageSearchInfo::CompiledDataPrivate &)
0641 {
0642     // copying invalidates the compiled data
0643     valid = false;
0644     return *this;
0645 }
0646 
0647 // vi:expandtab:tabstop=4 shiftwidth=4: