File indexing completed on 2024-04-28 16:31:56

0001 /***************************************************************************
0002     Copyright (C) 2001-2009 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include "document.h"
0026 #include "collectionfactory.h"
0027 #include "translators/tellicoimporter.h"
0028 #include "translators/tellicozipexporter.h"
0029 #include "translators/tellicoxmlexporter.h"
0030 #include "collection.h"
0031 #include "core/filehandler.h"
0032 #include "borrower.h"
0033 #include "fieldformat.h"
0034 #include "core/tellico_strings.h"
0035 #include "images/imagefactory.h"
0036 #include "images/imagedirectory.h"
0037 #include "images/image.h"
0038 #include "images/imageinfo.h"
0039 #include "utils/stringset.h"
0040 #include "utils/mergeconflictresolver.h"
0041 #include "progressmanager.h"
0042 #include "config/tellico_config.h"
0043 #include "entrycomparison.h"
0044 #include "utils/guiproxy.h"
0045 #include "tellico_debug.h"
0046 
0047 #include <KMessageBox>
0048 #include <KLocalizedString>
0049 
0050 #include <QApplication>
0051 
0052 using namespace Tellico;
0053 using Tellico::Data::Document;
0054 Document* Document::s_self = nullptr;
0055 
0056 Document::Document() : QObject(), m_coll(nullptr), m_isModified(false),
0057     m_loadAllImages(false), m_validFile(false), m_importer(nullptr), m_cancelImageWriting(true),
0058     m_fileFormat(Import::TellicoImporter::Unknown), m_loadImagesTimer(this) {
0059   m_allImagesOnDisk = Config::imageLocation() != Config::ImagesInFile;
0060   m_loadImagesTimer.setSingleShot(true);
0061   m_loadImagesTimer.setInterval(500);
0062   connect(&m_loadImagesTimer, &QTimer::timeout, this, &Document::slotLoadAllImages);
0063   newDocument(Collection::Book);
0064 }
0065 
0066 Document::~Document() {
0067   delete m_importer;
0068   m_importer = nullptr;
0069 }
0070 
0071 Tellico::Data::CollPtr Document::collection() const {
0072   return m_coll;
0073 }
0074 
0075 void Document::setURL(const QUrl& url_) {
0076   m_url = url_;
0077   if(m_url.fileName() != i18n(Tellico::untitledFilename)) {
0078     ImageFactory::setLocalDirectory(m_url);
0079     EntryComparison::setDocumentUrl(m_url);
0080   }
0081 }
0082 
0083 void Document::setModified(bool modified_) {
0084   if(modified_ != m_isModified) {
0085     m_isModified = modified_;
0086     emit signalModified(m_isModified);
0087   }
0088 }
0089 
0090 void Document::slotSetModified() {
0091   setModified(true);
0092 }
0093 
0094 /**
0095  * Since QUndoStack emits cleanChanged(), the behavior is opposite
0096  * the document modified flag
0097  */
0098 void Document::slotSetClean(bool clean_) {
0099   setModified(!clean_);
0100 }
0101 
0102 bool Document::newDocument(int type_) {
0103   if(m_importer) {
0104     m_importer->deleteLater();
0105     m_importer = nullptr;
0106   }
0107   deleteContents();
0108 
0109   m_coll = CollectionFactory::collection(type_, true);
0110   m_coll->setTrackGroups(true);
0111 
0112   emit signalCollectionAdded(m_coll);
0113   emit signalCollectionImagesLoaded(m_coll);
0114 
0115   setModified(false);
0116   QUrl url = QUrl::fromLocalFile(i18n(Tellico::untitledFilename));
0117   setURL(url);
0118   m_validFile = false;
0119   m_fileFormat = Import::TellicoImporter::Unknown;
0120 
0121   return true;
0122 }
0123 
0124 bool Document::openDocument(const QUrl& url_) {
0125   MARK;
0126   // delayed image loading only works for local files
0127   m_loadAllImages = !url_.isLocalFile();
0128   m_loadImagesTimer.stop(); // avoid potential race condition
0129 
0130   if(m_importer) {
0131     m_importer->deleteLater();
0132   }
0133   m_importer = new Import::TellicoImporter(url_, m_loadAllImages);
0134 
0135   ProgressItem& item = ProgressManager::self()->newProgressItem(m_importer, m_importer->progressLabel(), true);
0136   connect(m_importer, &Import::Importer::signalTotalSteps,
0137           ProgressManager::self(), &ProgressManager::setTotalSteps);
0138   connect(m_importer, &Import::Importer::signalProgress,
0139           ProgressManager::self(), &ProgressManager::setProgress);
0140   connect(&item, &ProgressItem::signalCancelled, m_importer, &Import::Importer::slotCancel);
0141   ProgressItem::Done done(m_importer);
0142 
0143   CollPtr coll = m_importer->collection();
0144   if(!m_importer) {
0145     myDebug() << "The importer was deleted out from under us";
0146     return false;
0147   }
0148   // delayed image loading only works for zip files
0149   // format is only known AFTER collection() is called
0150 
0151   m_fileFormat = m_importer->format();
0152   m_allImagesOnDisk = !m_importer->hasImages();
0153   if(!m_importer->hasImages() || m_fileFormat != Import::TellicoImporter::Zip) {
0154     m_loadAllImages = true;
0155   }
0156   ImageFactory::setZipArchive(m_importer->takeImages());
0157 
0158   if(!coll) {
0159 //    myDebug() << "returning false";
0160     GUI::Proxy::sorry(m_importer->statusMessage());
0161     m_validFile = false;
0162     return false;
0163   }
0164   deleteContents();
0165   m_coll = coll;
0166   m_coll->setTrackGroups(true);
0167   setURL(url_);
0168   m_validFile = true;
0169 
0170   emit signalCollectionAdded(m_coll);
0171 
0172   // m_importer might have been deleted?
0173   setModified(m_importer && m_importer->modifiedOriginal());
0174 //  if(pruneImages()) {
0175 //    slotSetModified(true);
0176 //  }
0177   if(m_importer && m_importer->hasImages()) {
0178     m_cancelImageWriting = false;
0179     m_loadImagesTimer.start();
0180   } else {
0181     emit signalCollectionImagesLoaded(m_coll);
0182     if(m_importer) {
0183       m_importer->deleteLater();
0184       m_importer = nullptr;
0185     }
0186   }
0187   return true;
0188 }
0189 
0190 bool Document::saveDocument(const QUrl& url_, bool force_) {
0191   // FileHandler::queryExists calls FileHandler::writeBackupFile
0192   // so the only reason to check queryExists() is if the url to write to is different than the current one
0193   if(url_ == m_url) {
0194     if(!FileHandler::writeBackupFile(url_)) {
0195       return false;
0196     }
0197   } else {
0198     if(!force_ && !FileHandler::queryExists(url_)) {
0199       return false;
0200     }
0201   }
0202 
0203   // in case we're still loading images, give that a chance to cancel
0204   m_cancelImageWriting = true;
0205   qApp->processEvents();
0206 
0207   ProgressItem& item = ProgressManager::self()->newProgressItem(this, i18n("Saving file..."), false);
0208   ProgressItem::Done done(this);
0209 
0210   // will always save as zip file, no matter if has images or not
0211   int imageLocation = Config::imageLocation();
0212   bool includeImages = imageLocation == Config::ImagesInFile;
0213   int totalSteps;
0214   // write all images to disk cache if needed
0215   // have to do this before executing exporter in case
0216   // the user changed the imageInFile setting from Yes to No, in which
0217   // case saving will overwrite the old file that has the images in it!
0218   if(includeImages) {
0219     totalSteps = 10;
0220     item.setTotalSteps(totalSteps);
0221     // since TellicoZipExporter uses 100 steps, then it will get 100/110 of the total progress
0222   } else {
0223     totalSteps = 100;
0224     item.setTotalSteps(totalSteps);
0225     m_cancelImageWriting = false;
0226     writeAllImages(imageLocation == Config::ImagesInAppDir ? ImageFactory::DataDir : ImageFactory::LocalDir, url_);
0227   }
0228   QScopedPointer<Export::Exporter> exporter;
0229   if(m_fileFormat == Import::TellicoImporter::XML) {
0230     exporter.reset(new Export::TellicoXMLExporter(m_coll));
0231     static_cast<Export::TellicoXMLExporter*>(exporter.data())->setIncludeImages(includeImages);
0232   } else {
0233     exporter.reset(new Export::TellicoZipExporter(m_coll));
0234     static_cast<Export::TellicoZipExporter*>(exporter.data())->setIncludeImages(includeImages);
0235   }
0236   item.setProgress(int(0.8*totalSteps));
0237   exporter->setEntries(m_coll->entries());
0238   exporter->setURL(url_);
0239   // since we already asked about overwriting the file, force the save
0240   long opt = exporter->options() | Export::ExportForce | Export::ExportComplete | Export::ExportProgress;
0241   // only write the image sizes if they're known already
0242   opt &= ~Export::ExportImageSize;
0243   exporter->setOptions(opt);
0244   const bool success = exporter->exec();
0245   item.setProgress(int(0.9*totalSteps));
0246 
0247   if(success) {
0248     setURL(url_);
0249     // if successful, doc is no longer modified
0250     setModified(false);
0251   } else {
0252     myDebug() << "Document::saveDocument() - not successful saving to" << url_.url();
0253   }
0254   return success;
0255 }
0256 
0257 bool Document::closeDocument() {
0258   if(m_importer) {
0259     m_importer->deleteLater();
0260     m_importer = nullptr;
0261   }
0262   deleteContents();
0263   return true;
0264 }
0265 
0266 void Document::deleteContents() {
0267   if(m_coll) {
0268     emit signalCollectionDeleted(m_coll);
0269   }
0270   // don't delete the m_importer here, bad things will happen
0271 
0272   // since the collection holds a pointer to each entry and each entry
0273   // hold a pointer to the collection, and they're both sharedptrs,
0274   // neither will ever get deleted, unless the entries are removed from the collection
0275   if(m_coll) {
0276     m_coll->clear();
0277   }
0278   m_coll = nullptr; // old collection gets deleted as refcount goes to 0
0279   m_cancelImageWriting = true;
0280 }
0281 
0282 void Document::appendCollection(Tellico::Data::CollPtr coll_, bool* structuralChange_) {
0283   appendCollection(m_coll, coll_, structuralChange_);
0284 }
0285 
0286 void Document::appendCollection(Tellico::Data::CollPtr coll1_, Tellico::Data::CollPtr coll2_, bool* structuralChange_) {
0287   if(structuralChange_) *structuralChange_ = false;
0288   if(!coll1_ || !coll2_) {
0289     return;
0290   }
0291 
0292   coll1_->blockSignals(true);
0293 
0294   foreach(FieldPtr field, coll2_->fields()) {
0295     bool collChange = coll1_->mergeField(field);
0296     if(collChange && structuralChange_) *structuralChange_ = true;
0297   }
0298 
0299   Data::EntryList newEntries;
0300   foreach(EntryPtr entry, coll2_->entries()) {
0301     Data::EntryPtr newEntry(new Data::Entry(*entry));
0302     newEntry->setCollection(coll1_);
0303     newEntries << newEntry;
0304   }
0305   coll1_->addEntries(newEntries);
0306   // TODO: merge filters and loans
0307   coll1_->blockSignals(false);
0308 }
0309 
0310 Tellico::Data::MergePair Document::mergeCollection(Tellico::Data::CollPtr coll_, bool* structuralChange_) {
0311   return mergeCollection(m_coll, coll_, structuralChange_);
0312 }
0313 
0314 Tellico::Data::MergePair Document::mergeCollection(Tellico::Data::CollPtr coll1_, Tellico::Data::CollPtr coll2_, bool* structuralChange_) {
0315   if(structuralChange_) *structuralChange_ = false;
0316   MergePair pair;
0317   if(!coll1_ || !coll2_) {
0318     return pair;
0319   }
0320 
0321   coll1_->blockSignals(true);
0322   Data::FieldList fields = coll2_->fields();
0323   foreach(FieldPtr field, fields) {
0324     bool collChange = coll1_->mergeField(field);
0325     if(collChange && structuralChange_) *structuralChange_ = true;
0326   }
0327 
0328   EntryList currEntries = coll1_->entries();
0329   EntryList newEntries = coll2_->entries();
0330   std::sort(currEntries.begin(), currEntries.end(), Data::EntryCmp(QStringLiteral("title")));
0331   std::sort(newEntries.begin(), newEntries.end(), Data::EntryCmp(QStringLiteral("title")));
0332 
0333   const int currTotal = currEntries.count();
0334   int lastMatchId = 0;
0335   bool checkSameId = false; // if the matching entries have the same id, then check that first for later comparisons
0336   foreach(EntryPtr newEntry, newEntries) {
0337     int bestMatch = 0;
0338     Data::EntryPtr matchEntry, currEntry;
0339     // first, if we're checking against same ID
0340     if(checkSameId) {
0341       currEntry = coll1_->entryById(newEntry->id());
0342       if(currEntry && coll1_->sameEntry(currEntry, newEntry) >= EntryComparison::ENTRY_PERFECT_MATCH) {
0343         // only have to compare against perfect match
0344         matchEntry = currEntry;
0345       }
0346     }
0347     if(!matchEntry) {
0348       // alternative is to loop over them all
0349       for(int i = 0; i < currTotal; ++i) {
0350         // since we're sorted by title, track the index of the previous match and start comparison there
0351         currEntry = currEntries.at((i+lastMatchId) % currTotal);
0352         const int match = coll1_->sameEntry(currEntry, newEntry);
0353         if(match >= EntryComparison::ENTRY_PERFECT_MATCH) {
0354           matchEntry = currEntry;
0355           lastMatchId = (i+lastMatchId) % currTotal;
0356           break;
0357         } else if(match >= EntryComparison::ENTRY_GOOD_MATCH && match > bestMatch) {
0358           bestMatch = match;
0359           matchEntry = currEntry;
0360           lastMatchId = (i+lastMatchId) % currTotal;
0361           // don't break, keep looking for better one
0362         }
0363       }
0364     }
0365     if(matchEntry) {
0366       checkSameId = checkSameId || (matchEntry->id() == newEntry->id());
0367       Merge::mergeEntry(matchEntry, newEntry);
0368     } else {
0369       Data::EntryPtr e(new Data::Entry(*newEntry));
0370       e->setCollection(coll1_);
0371       // keep track of which entries got added
0372       pair.first.append(e);
0373     }
0374   }
0375   coll1_->addEntries(pair.first);
0376   // TODO: merge filters and loans
0377   coll1_->blockSignals(false);
0378   return pair;
0379 }
0380 
0381 void Document::replaceCollection(Tellico::Data::CollPtr coll_) {
0382   if(!coll_) {
0383     return;
0384   }
0385 
0386   QUrl url = QUrl::fromLocalFile(i18n(Tellico::untitledFilename));
0387   setURL(url);
0388   m_validFile = false;
0389 
0390   // the collection gets cleared by the CollectionCommand that called this function
0391   // no need to do it here
0392 
0393   m_coll = coll_;
0394   m_coll->setTrackGroups(true);
0395   m_cancelImageWriting = true;
0396   // CollectionCommand takes care of calling Controller signals
0397 }
0398 
0399 void Document::unAppendCollection(Tellico::Data::FieldList origFields_, QList<int> addedEntries_) {
0400   m_coll->blockSignals(true);
0401 
0402   StringSet origFieldNames;
0403   foreach(FieldPtr field, origFields_) {
0404     m_coll->modifyField(field);
0405     origFieldNames.add(field->name());
0406   }
0407 
0408   EntryList entriesToRemove;
0409   foreach(int id, addedEntries_) {
0410     auto e = m_coll->entryById(id);
0411     if(e) entriesToRemove << e;
0412   }
0413   m_coll->removeEntries(entriesToRemove);
0414 
0415   // since Collection::removeField() iterates over all entries to reset the value of the field
0416   // don't removeField() until after removeEntry() is done
0417   FieldList currFields = m_coll->fields();
0418   foreach(FieldPtr field, currFields) {
0419     if(!origFieldNames.has(field->name())) {
0420       m_coll->removeField(field);
0421     }
0422   }
0423   m_coll->blockSignals(false);
0424 }
0425 
0426 void Document::unMergeCollection(Tellico::Data::FieldList origFields_, Tellico::Data::MergePair entryPair_) {
0427   m_coll->blockSignals(true);
0428 
0429   QStringList origFieldNames;
0430   foreach(FieldPtr field, origFields_) {
0431     m_coll->modifyField(field);
0432     origFieldNames << field->name();
0433   }
0434 
0435   // first item in pair are the entries added by the operation, remove them
0436   EntryList entries = entryPair_.first;
0437   m_coll->removeEntries(entries);
0438 
0439   // second item in pair are the entries which got modified by the original merge command
0440   const QString track = QStringLiteral("track");
0441   PairVector trackChanges = entryPair_.second;
0442   // need to go through them in reverse since one entry may have been modified multiple times
0443   // first item in the pair is the entry pointer
0444   // second item is the old value of the track field
0445   for(int i = trackChanges.count()-1; i >= 0; --i) {
0446     trackChanges[i].first->setField(track, trackChanges[i].second);
0447   }
0448 
0449   // since Collection::removeField() iterates over all entries to reset the value of the field
0450   // don't removeField() until after removeEntry() is done
0451   FieldList currFields = m_coll->fields();
0452   foreach(FieldPtr field, currFields) {
0453     if(origFieldNames.indexOf(field->name()) == -1) {
0454       m_coll->removeField(field);
0455     }
0456   }
0457   m_coll->blockSignals(false);
0458 }
0459 
0460 bool Document::isEmpty() const {
0461   //an empty doc may contain a collection, but no entries
0462   return (!m_coll || m_coll->entries().isEmpty());
0463 }
0464 
0465 bool Document::loadAllImagesNow() const {
0466 //  DEBUG_LINE;
0467   if(!m_coll || !m_validFile) {
0468     return false;
0469   }
0470   if(m_loadAllImages) {
0471     myDebug() << "Document::loadAllImagesNow() - all valid images should already be loaded!";
0472     return false;
0473   }
0474   return Import::TellicoImporter::loadAllImages(m_url);
0475 }
0476 
0477 Tellico::Data::EntryList Document::filteredEntries(Tellico::FilterPtr filter_) const {
0478   Data::EntryList matches;
0479   Data::EntryList entries = m_coll->entries();
0480   foreach(EntryPtr entry, entries) {
0481     if(filter_->matches(entry)) {
0482       matches.append(entry);
0483     }
0484   }
0485   return matches;
0486 }
0487 
0488 void Document::checkOutEntry(Tellico::Data::EntryPtr entry_) {
0489   if(!entry_) {
0490     return;
0491   }
0492 
0493   const QString loaned = QStringLiteral("loaned");
0494   if(!m_coll->hasField(loaned)) {
0495     FieldPtr f(new Field(loaned, i18n("Loaned"), Field::Bool));
0496     f->setFlags(Field::AllowGrouped);
0497     f->setCategory(i18n("Personal"));
0498     m_coll->addField(f);
0499   }
0500   entry_->setField(loaned, QStringLiteral("true"));
0501   EntryList vec;
0502   vec.append(entry_);
0503   m_coll->updateDicts(vec, QStringList() << loaned);
0504 }
0505 
0506 void Document::checkInEntry(Tellico::Data::EntryPtr entry_) {
0507   if(!entry_) {
0508     return;
0509   }
0510 
0511   const QString loaned = QStringLiteral("loaned");
0512   if(!m_coll->hasField(loaned)) {
0513     return;
0514   }
0515   entry_->setField(loaned, QString());
0516   m_coll->updateDicts(EntryList() << entry_, QStringList() << loaned);
0517 }
0518 
0519 void Document::renameCollection(const QString& newTitle_) {
0520   m_coll->setTitle(newTitle_);
0521 }
0522 
0523 // this only gets called when a zip file with images is opened
0524 // by loading every image, it gets pulled out of the zip file and
0525 // copied to disk. Then the zip file can be closed and not retained in memory
0526 void Document::slotLoadAllImages() {
0527   QString id;
0528   StringSet images;
0529   foreach(EntryPtr entry, m_coll->entries()) {
0530     foreach(FieldPtr field, m_coll->imageFields()) {
0531       id = entry->field(field);
0532       if(id.isEmpty() || images.has(id)) {
0533         continue;
0534       }
0535       // this is the early loading, so just by calling imageById()
0536       // the image gets sucked from the zip file and written to disk
0537       // by ImageFactory::imageById()
0538       // TODO:: does this need to check against images with link only?
0539       if(ImageFactory::imageById(id).isNull()) {
0540         myDebug() << "Null image for entry:" << entry->title() << id;
0541       }
0542       images.add(id);
0543       if(m_cancelImageWriting) {
0544         break;
0545       }
0546     }
0547     if(m_cancelImageWriting) {
0548       break;
0549     }
0550     // stay responsive, do this in the background
0551     qApp->processEvents();
0552   }
0553 
0554   if(m_cancelImageWriting) {
0555     myLog() << "slotLoadAllImages() - cancel image writing";
0556   } else {
0557     emit signalCollectionImagesLoaded(m_coll);
0558   }
0559 
0560   m_cancelImageWriting = false;
0561   if(m_importer) {
0562     m_importer->deleteLater();
0563     m_importer = nullptr;
0564   }
0565 }
0566 
0567 // cacheDir_ is the location dir to write the images
0568 // localDir_ provide the new file location which is only needed if cacheDir == LocalDir
0569 void Document::writeAllImages(int cacheDir_, const QUrl& localDir_) {
0570   // images get 80 steps in saveDocument()
0571   const uint stepSize = 1 + qMax(1, m_coll->entryCount()/80); // add 1 since it could round off
0572   uint j = 1;
0573 
0574   ImageFactory::CacheDir cacheDir = static_cast<ImageFactory::CacheDir>(cacheDir_);
0575   QScopedPointer<ImageDirectory> imgDir;
0576   if(cacheDir == ImageFactory::LocalDir) {
0577     imgDir.reset(new ImageDirectory(ImageFactory::localDirectory(localDir_)));
0578   }
0579 
0580   QString id;
0581   StringSet images;
0582   EntryList entries = m_coll->entries();
0583   FieldList imageFields = m_coll->imageFields();
0584   foreach(EntryPtr entry, entries) {
0585     foreach(FieldPtr field, imageFields) {
0586       id = entry->field(field);
0587       if(id.isEmpty() || images.has(id)) {
0588         continue;
0589       }
0590       images.add(id);
0591       if(ImageFactory::imageInfo(id).linkOnly) {
0592         continue;
0593       }
0594       // careful here, if we're writing to LocalDir, need to read from the old LocalDir and write to new
0595       bool success;
0596       if(cacheDir == ImageFactory::LocalDir) {
0597         success = ImageFactory::writeCachedImage(id, imgDir.data());
0598       } else {
0599         success = ImageFactory::writeCachedImage(id, cacheDir);
0600       }
0601       if(!success) {
0602         myDebug() << "did not write image for entry title:" << entry->title();
0603       }
0604       if(m_cancelImageWriting) {
0605         break;
0606       }
0607     }
0608     if(j%stepSize == 0) {
0609       ProgressManager::self()->setProgress(this, j/stepSize);
0610     }
0611     ++j;
0612     if(m_cancelImageWriting) {
0613       break;
0614     }
0615   }
0616 
0617   if(m_cancelImageWriting) {
0618     myDebug() << "Document::writeAllImages() - cancel image writing";
0619   }
0620 
0621   m_cancelImageWriting = false;
0622 }
0623 
0624 bool Document::pruneImages() {
0625   bool found = false;
0626   QString id;
0627   StringSet images;
0628   Data::EntryList entries = m_coll->entries();
0629   Data::FieldList imageFields = m_coll->imageFields();
0630   foreach(EntryPtr entry, entries) {
0631     foreach(FieldPtr field, imageFields) {
0632       id = entry->field(field);
0633       if(id.isEmpty() || images.has(id)) {
0634         continue;
0635       }
0636       const Data::Image& img = ImageFactory::imageById(id);
0637       if(img.isNull()) {
0638         entry->setField(field, QString());
0639         found = true;
0640         myDebug() << "removing null image for" << entry->title() << ":" << id;
0641       } else {
0642         images.add(id);
0643       }
0644     }
0645   }
0646   return found;
0647 }
0648 
0649 int Document::imageCount() const {
0650   if(!m_coll) {
0651     return 0;
0652   }
0653   StringSet images;
0654   FieldList fields = m_coll->imageFields();
0655   EntryList entries = m_coll->entries();
0656   foreach(FieldPtr field, fields) {
0657     foreach(EntryPtr entry, entries) {
0658       images.add(entry->field(field));
0659     }
0660   }
0661   return images.count();
0662 }
0663 
0664 void Document::removeImagesNotInCollection(Tellico::Data::EntryList entries_, Tellico::Data::EntryList entriesToKeep_) {
0665   // first get list of all images in collection
0666   StringSet images;
0667   FieldList fields = m_coll->imageFields();
0668   EntryList allEntries = m_coll->entries();
0669   foreach(FieldPtr field, fields) {
0670     foreach(EntryPtr entry, allEntries) {
0671       images.add(entry->field(field));
0672     }
0673     foreach(EntryPtr entry, entriesToKeep_) {
0674       images.add(entry->field(field));
0675     }
0676   }
0677 
0678   // now for all images not in the cache, we can clear them
0679   StringSet imagesToCheck = ImageFactory::imagesNotInCache();
0680 
0681   // if entries_ is not empty, that means we want to limit the images removed
0682   // to those that are referenced in those entries
0683   StringSet imagesToRemove;
0684   foreach(FieldPtr field, fields) {
0685     foreach(EntryPtr entry, entries_) {
0686       QString id = entry->field(field);
0687       if(!id.isEmpty() && imagesToCheck.has(id) && !images.has(id)) {
0688         imagesToRemove.add(id);
0689       }
0690     }
0691   }
0692 
0693   const QStringList realImagesToRemove = imagesToRemove.values();
0694   for(QStringList::ConstIterator it = realImagesToRemove.begin(); it != realImagesToRemove.end(); ++it) {
0695     ImageFactory::removeImage(*it, false); // doesn't delete, just remove link
0696   }
0697 }