File indexing completed on 2024-04-28 05:08:18

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   // be sure to set the new Url before signalling a new collection
0113   // since reading options for custom collections depend on the file name
0114   const QUrl url = QUrl::fromLocalFile(i18n(Tellico::untitledFilename));
0115   setURL(url);
0116 
0117   emit signalCollectionAdded(m_coll);
0118   emit signalCollectionImagesLoaded(m_coll);
0119 
0120   setModified(false);
0121   m_validFile = false;
0122   m_fileFormat = Import::TellicoImporter::Unknown;
0123 
0124   return true;
0125 }
0126 
0127 bool Document::openDocument(const QUrl& url_) {
0128   MARK;
0129   if(url_.isEmpty()) {
0130     return false;
0131   }
0132   // delayed image loading only works for local files
0133   m_loadAllImages = !url_.isLocalFile();
0134   m_loadImagesTimer.stop(); // avoid potential race condition
0135 
0136   if(m_importer) {
0137     m_importer->deleteLater();
0138   }
0139   m_importer = new Import::TellicoImporter(url_, m_loadAllImages);
0140 
0141   ProgressItem& item = ProgressManager::self()->newProgressItem(m_importer, m_importer->progressLabel(), true);
0142   connect(m_importer, &Import::Importer::signalTotalSteps,
0143           ProgressManager::self(), &ProgressManager::setTotalSteps);
0144   connect(m_importer, &Import::Importer::signalProgress,
0145           ProgressManager::self(), &ProgressManager::setProgress);
0146   connect(&item, &ProgressItem::signalCancelled, m_importer, &Import::Importer::slotCancel);
0147   ProgressItem::Done done(m_importer);
0148 
0149   CollPtr coll = m_importer->collection();
0150   if(!m_importer) {
0151     myDebug() << "The importer was deleted out from under us";
0152     return false;
0153   }
0154   // delayed image loading only works for zip files
0155   // format is only known AFTER collection() is called
0156 
0157   m_fileFormat = m_importer->format();
0158   m_allImagesOnDisk = !m_importer->hasImages();
0159   if(!m_importer->hasImages() || m_fileFormat != Import::TellicoImporter::Zip) {
0160     m_loadAllImages = true;
0161   }
0162   ImageFactory::setZipArchive(m_importer->takeImages());
0163 
0164   if(!coll) {
0165     myLog() << "Failed to read a collection" << m_importer->statusMessage();
0166     GUI::Proxy::sorry(m_importer->statusMessage());
0167     m_validFile = false;
0168     return false;
0169   }
0170   deleteContents();
0171   m_coll = coll;
0172   m_coll->setTrackGroups(true);
0173   setURL(url_);
0174   m_validFile = true;
0175 
0176   emit signalCollectionAdded(m_coll);
0177 
0178   // m_importer might have been deleted?
0179   setModified(m_importer && m_importer->modifiedOriginal());
0180 //  if(pruneImages()) {
0181 //    slotSetModified(true);
0182 //  }
0183   if(m_importer && m_importer->hasImages()) {
0184     m_cancelImageWriting = false;
0185     m_loadImagesTimer.start();
0186   } else {
0187     emit signalCollectionImagesLoaded(m_coll);
0188     if(m_importer) {
0189       m_importer->deleteLater();
0190       m_importer = nullptr;
0191     }
0192   }
0193   return true;
0194 }
0195 
0196 bool Document::saveDocument(const QUrl& url_, bool force_) {
0197   // FileHandler::queryExists calls FileHandler::writeBackupFile
0198   // so the only reason to check queryExists() is if the url to write to is different than the current one
0199   if(url_ == m_url) {
0200     if(!FileHandler::writeBackupFile(url_)) {
0201       return false;
0202     }
0203   } else {
0204     if(!force_ && !FileHandler::queryExists(url_)) {
0205       return false;
0206     }
0207   }
0208 
0209   // in case we're still loading images, give that a chance to cancel
0210   m_cancelImageWriting = true;
0211   qApp->processEvents();
0212 
0213   ProgressItem& item = ProgressManager::self()->newProgressItem(this, i18n("Saving file..."), false);
0214   ProgressItem::Done done(this);
0215 
0216   // will always save as zip file, no matter if has images or not
0217   int imageLocation = Config::imageLocation();
0218   bool includeImages = imageLocation == Config::ImagesInFile;
0219   int totalSteps;
0220   // write all images to disk cache if needed
0221   // have to do this before executing exporter in case
0222   // the user changed the imageInFile setting from Yes to No, in which
0223   // case saving will overwrite the old file that has the images in it!
0224   if(includeImages) {
0225     totalSteps = 10;
0226     item.setTotalSteps(totalSteps);
0227     // since TellicoZipExporter uses 100 steps, then it will get 100/110 of the total progress
0228   } else {
0229     totalSteps = 100;
0230     item.setTotalSteps(totalSteps);
0231     m_cancelImageWriting = false;
0232     writeAllImages(imageLocation == Config::ImagesInAppDir ? ImageFactory::DataDir : ImageFactory::LocalDir, url_);
0233   }
0234   QScopedPointer<Export::Exporter> exporter;
0235   if(m_fileFormat == Import::TellicoImporter::XML) {
0236     exporter.reset(new Export::TellicoXMLExporter(m_coll));
0237     static_cast<Export::TellicoXMLExporter*>(exporter.data())->setIncludeImages(includeImages);
0238   } else {
0239     exporter.reset(new Export::TellicoZipExporter(m_coll));
0240     static_cast<Export::TellicoZipExporter*>(exporter.data())->setIncludeImages(includeImages);
0241   }
0242   item.setProgress(int(0.8*totalSteps));
0243   exporter->setEntries(m_coll->entries());
0244   exporter->setURL(url_);
0245   // since we already asked about overwriting the file, force the save
0246   long opt = exporter->options() | Export::ExportForce | Export::ExportComplete | Export::ExportProgress;
0247   // only write the image sizes if they're known already
0248   opt &= ~Export::ExportImageSize;
0249   exporter->setOptions(opt);
0250   const bool success = exporter->exec();
0251   item.setProgress(int(0.9*totalSteps));
0252 
0253   if(success) {
0254     setURL(url_);
0255     // if successful, doc is no longer modified
0256     setModified(false);
0257   } else {
0258     myLog() << "Document::saveDocument() - not successful saving to" << url_.toDisplayString(QUrl::PreferLocalFile);
0259   }
0260   return success;
0261 }
0262 
0263 bool Document::saveDocumentTemplate(const QUrl& url_, const QString& title_) {
0264   Data::CollPtr collTemplate = CollectionFactory::collection(m_coll->type(), false /* no default fields */);
0265   collTemplate->setTitle(title_);
0266   // add the fields from the current collection
0267   foreach(auto field, m_coll->fields()) {
0268     collTemplate->addField(field);
0269   }
0270   foreach(auto filter, m_coll->filters()) {
0271     collTemplate->addFilter(filter);
0272   }
0273   QScopedPointer<Export::Exporter> exporter(new Export::TellicoXMLExporter(collTemplate));
0274   exporter->setURL(url_);
0275   // since we already asked about overwriting the file, force the save
0276   exporter->setOptions(exporter->options() | Export::ExportForce | Export::ExportComplete);
0277   return exporter->exec();
0278 }
0279 
0280 bool Document::closeDocument() {
0281   if(m_importer) {
0282     m_importer->deleteLater();
0283     m_importer = nullptr;
0284   }
0285   deleteContents();
0286   return true;
0287 }
0288 
0289 void Document::deleteContents() {
0290   if(m_coll) {
0291     emit signalCollectionDeleted(m_coll);
0292   }
0293   // don't delete the m_importer here, bad things will happen
0294 
0295   // since the collection holds a pointer to each entry and each entry
0296   // hold a pointer to the collection, and they're both sharedptrs,
0297   // neither will ever get deleted, unless the entries are removed from the collection
0298   if(m_coll) {
0299     m_coll->clear();
0300   }
0301   m_coll = nullptr; // old collection gets deleted as refcount goes to 0
0302   m_cancelImageWriting = true;
0303 }
0304 
0305 void Document::appendCollection(Tellico::Data::CollPtr coll_) {
0306   bool structuralChange = false;
0307   appendCollection(m_coll, coll_, &structuralChange);
0308   emit signalCollectionModified(m_coll, structuralChange);
0309 }
0310 
0311 void Document::appendCollection(Tellico::Data::CollPtr coll1_, Tellico::Data::CollPtr coll2_, bool* structuralChange_) {
0312   if(structuralChange_) *structuralChange_ = false;
0313   if(!coll1_ || !coll2_) {
0314     return;
0315   }
0316 
0317   coll1_->blockSignals(true);
0318 
0319   foreach(FieldPtr field, coll2_->fields()) {
0320     bool collChange = coll1_->mergeField(field);
0321     if(collChange && structuralChange_) *structuralChange_ = true;
0322   }
0323 
0324   Data::EntryList newEntries;
0325   foreach(EntryPtr entry, coll2_->entries()) {
0326     Data::EntryPtr newEntry(new Data::Entry(*entry));
0327     newEntry->setCollection(coll1_);
0328     newEntries << newEntry;
0329   }
0330   coll1_->addEntries(newEntries);
0331   // TODO: merge filters and loans
0332   coll1_->blockSignals(false);
0333 }
0334 
0335 Tellico::Data::MergePair Document::mergeCollection(Tellico::Data::CollPtr coll_) {
0336   bool structuralChange = false;
0337   const auto mergeResult = mergeCollection(m_coll, coll_, &structuralChange);
0338   emit signalCollectionModified(m_coll, structuralChange);
0339   return mergeResult;
0340 }
0341 
0342 Tellico::Data::MergePair Document::mergeCollection(Tellico::Data::CollPtr coll1_, Tellico::Data::CollPtr coll2_, bool* structuralChange_) {
0343   if(structuralChange_) *structuralChange_ = false;
0344   MergePair pair;
0345   if(!coll1_ || !coll2_) {
0346     return pair;
0347   }
0348 
0349   coll1_->blockSignals(true);
0350   Data::FieldList fields = coll2_->fields();
0351   foreach(FieldPtr field, fields) {
0352     bool collChange = coll1_->mergeField(field);
0353     if(collChange && structuralChange_) *structuralChange_ = true;
0354   }
0355 
0356   EntryList currEntries = coll1_->entries();
0357   EntryList newEntries = coll2_->entries();
0358   std::sort(currEntries.begin(), currEntries.end(), Data::EntryCmp(QStringLiteral("title")));
0359   std::sort(newEntries.begin(), newEntries.end(), Data::EntryCmp(QStringLiteral("title")));
0360 
0361   const int currTotal = currEntries.count();
0362   int lastMatchId = 0;
0363   bool checkSameId = false; // if the matching entries have the same id, then check that first for later comparisons
0364   foreach(EntryPtr newEntry, newEntries) {
0365     int bestMatch = 0;
0366     Data::EntryPtr matchEntry, currEntry;
0367     // first, if we're checking against same ID
0368     if(checkSameId) {
0369       currEntry = coll1_->entryById(newEntry->id());
0370       if(currEntry && coll1_->sameEntry(currEntry, newEntry) >= EntryComparison::ENTRY_PERFECT_MATCH) {
0371         // only have to compare against perfect match
0372         matchEntry = currEntry;
0373       }
0374     }
0375     if(!matchEntry) {
0376       // alternative is to loop over them all
0377       for(int i = 0; i < currTotal; ++i) {
0378         // since we're sorted by title, track the index of the previous match and start comparison there
0379         currEntry = currEntries.at((i+lastMatchId) % currTotal);
0380         const int match = coll1_->sameEntry(currEntry, newEntry);
0381         if(match >= EntryComparison::ENTRY_PERFECT_MATCH) {
0382           matchEntry = currEntry;
0383           lastMatchId = (i+lastMatchId) % currTotal;
0384           break;
0385         } else if(match >= EntryComparison::ENTRY_GOOD_MATCH && match > bestMatch) {
0386           bestMatch = match;
0387           matchEntry = currEntry;
0388           lastMatchId = (i+lastMatchId) % currTotal;
0389           // don't break, keep looking for better one
0390         }
0391       }
0392     }
0393     if(matchEntry) {
0394       checkSameId = checkSameId || (matchEntry->id() == newEntry->id());
0395       Merge::mergeEntry(matchEntry, newEntry);
0396     } else {
0397       Data::EntryPtr e(new Data::Entry(*newEntry));
0398       e->setCollection(coll1_);
0399       // keep track of which entries got added
0400       pair.first.append(e);
0401     }
0402   }
0403   coll1_->addEntries(pair.first);
0404   // TODO: merge filters and loans
0405   coll1_->blockSignals(false);
0406   return pair;
0407 }
0408 
0409 void Document::replaceCollection(Tellico::Data::CollPtr coll_) {
0410   if(!coll_) {
0411     return;
0412   }
0413 
0414   QUrl url = QUrl::fromLocalFile(i18n(Tellico::untitledFilename));
0415   setURL(url);
0416   m_validFile = false;
0417 
0418   emit signalCollectionDeleted(m_coll);
0419   m_coll = coll_;
0420   m_coll->setTrackGroups(true);
0421   m_cancelImageWriting = true;
0422   emit signalCollectionAdded(m_coll);
0423 }
0424 
0425 void Document::unAppendCollection(Tellico::Data::FieldList origFields_, QList<int> addedEntries_) {
0426   m_coll->blockSignals(true);
0427   bool structuralChange = false;
0428 
0429   StringSet origFieldNames;
0430   foreach(FieldPtr field, origFields_) {
0431     m_coll->modifyField(field);
0432     origFieldNames.add(field->name());
0433     structuralChange = true;
0434   }
0435 
0436   EntryList entriesToRemove;
0437   foreach(int id, addedEntries_) {
0438     auto e = m_coll->entryById(id);
0439     if(e) entriesToRemove << e;
0440   }
0441   m_coll->removeEntries(entriesToRemove);
0442 
0443   // since Collection::removeField() iterates over all entries to reset the value of the field
0444   // don't removeField() until after removeEntry() is done
0445   FieldList currFields = m_coll->fields();
0446   foreach(FieldPtr field, currFields) {
0447     if(!origFieldNames.has(field->name())) {
0448       m_coll->removeField(field);
0449       structuralChange = true;
0450     }
0451   }
0452   m_coll->blockSignals(false);
0453   emit signalCollectionModified(m_coll, structuralChange);
0454 }
0455 
0456 void Document::unMergeCollection(Tellico::Data::FieldList origFields_, Tellico::Data::MergePair entryPair_) {
0457   m_coll->blockSignals(true);
0458   bool structuralChange = false;
0459 
0460   QStringList origFieldNames;
0461   foreach(FieldPtr field, origFields_) {
0462     m_coll->modifyField(field);
0463     origFieldNames << field->name();
0464     structuralChange = true;
0465   }
0466 
0467   // first item in pair are the entries added by the operation, remove them
0468   EntryList entries = entryPair_.first;
0469   m_coll->removeEntries(entries);
0470 
0471   // second item in pair are the entries which got modified by the original merge command
0472   const QString track = QStringLiteral("track");
0473   PairVector trackChanges = entryPair_.second;
0474   // need to go through them in reverse since one entry may have been modified multiple times
0475   // first item in the pair is the entry pointer
0476   // second item is the old value of the track field
0477   for(int i = trackChanges.count()-1; i >= 0; --i) {
0478     trackChanges[i].first->setField(track, trackChanges[i].second);
0479   }
0480 
0481   // since Collection::removeField() iterates over all entries to reset the value of the field
0482   // don't removeField() until after removeEntry() is done
0483   FieldList currFields = m_coll->fields();
0484   foreach(FieldPtr field, currFields) {
0485     if(origFieldNames.indexOf(field->name()) == -1) {
0486       m_coll->removeField(field);
0487       structuralChange = true;
0488     }
0489   }
0490   m_coll->blockSignals(false);
0491   emit signalCollectionModified(m_coll, structuralChange);
0492 }
0493 
0494 bool Document::isEmpty() const {
0495   //an empty doc may contain a collection, but no entries
0496   return (!m_coll || m_coll->entries().isEmpty());
0497 }
0498 
0499 bool Document::loadAllImagesNow() const {
0500 //  DEBUG_LINE;
0501   if(!m_coll || !m_validFile) {
0502     return false;
0503   }
0504   if(m_loadAllImages) {
0505     myDebug() << "Document::loadAllImagesNow() - all valid images should already be loaded!";
0506     return false;
0507   }
0508   return Import::TellicoImporter::loadAllImages(m_url);
0509 }
0510 
0511 Tellico::Data::EntryList Document::filteredEntries(Tellico::FilterPtr filter_) const {
0512   Data::EntryList matches;
0513   Data::EntryList entries = m_coll->entries();
0514   foreach(EntryPtr entry, entries) {
0515     if(filter_->matches(entry)) {
0516       matches.append(entry);
0517     }
0518   }
0519   return matches;
0520 }
0521 
0522 void Document::checkOutEntry(Tellico::Data::EntryPtr entry_) {
0523   if(!entry_) {
0524     return;
0525   }
0526 
0527   const QString loaned = QStringLiteral("loaned");
0528   if(!m_coll->hasField(loaned)) {
0529     FieldPtr f(new Field(loaned, i18n("Loaned"), Field::Bool));
0530     f->setFlags(Field::AllowGrouped);
0531     f->setCategory(i18n("Personal"));
0532     m_coll->addField(f);
0533   }
0534   entry_->setField(loaned, QStringLiteral("true"));
0535   EntryList vec;
0536   vec.append(entry_);
0537   m_coll->updateDicts(vec, QStringList() << loaned);
0538 }
0539 
0540 void Document::checkInEntry(Tellico::Data::EntryPtr entry_) {
0541   if(!entry_) {
0542     return;
0543   }
0544 
0545   const QString loaned = QStringLiteral("loaned");
0546   if(!m_coll->hasField(loaned)) {
0547     return;
0548   }
0549   entry_->setField(loaned, QString());
0550   m_coll->updateDicts(EntryList() << entry_, QStringList() << loaned);
0551 }
0552 
0553 void Document::renameCollection(const QString& newTitle_) {
0554   m_coll->setTitle(newTitle_);
0555 }
0556 
0557 // this only gets called when a zip file with images is opened
0558 // by loading every image, it gets pulled out of the zip file and
0559 // copied to disk. Then the zip file can be closed and not retained in memory
0560 void Document::slotLoadAllImages() {
0561   QString id;
0562   StringSet images;
0563   foreach(EntryPtr entry, m_coll->entries()) {
0564     foreach(FieldPtr field, m_coll->imageFields()) {
0565       id = entry->field(field);
0566       if(id.isEmpty() || images.has(id)) {
0567         continue;
0568       }
0569       // this is the early loading, so just by calling imageById()
0570       // the image gets sucked from the zip file and written to disk
0571       // by ImageFactory::imageById()
0572       // TODO:: does this need to check against images with link only?
0573       if(ImageFactory::imageById(id).isNull()) {
0574         myLog() << "Null image for entry:" << entry->title() << id;
0575       }
0576       images.add(id);
0577       if(m_cancelImageWriting) {
0578         break;
0579       }
0580     }
0581     if(m_cancelImageWriting) {
0582       break;
0583     }
0584     // stay responsive, do this in the background
0585     qApp->processEvents();
0586   }
0587 
0588   if(m_cancelImageWriting) {
0589     myLog() << "slotLoadAllImages() - cancel image writing";
0590   } else {
0591     emit signalCollectionImagesLoaded(m_coll);
0592   }
0593 
0594   m_cancelImageWriting = false;
0595   if(m_importer) {
0596     m_importer->deleteLater();
0597     m_importer = nullptr;
0598   }
0599 }
0600 
0601 // cacheDir_ is the location dir to write the images
0602 // localDir_ provide the new file location which is only needed if cacheDir == LocalDir
0603 void Document::writeAllImages(int cacheDir_, const QUrl& localDir_) {
0604   // images get 80 steps in saveDocument()
0605   const uint stepSize = 1 + qMax(1, m_coll->entryCount()/80); // add 1 since it could round off
0606   uint j = 1;
0607 
0608   ImageFactory::CacheDir cacheDir = static_cast<ImageFactory::CacheDir>(cacheDir_);
0609   QScopedPointer<ImageDirectory> imgDir;
0610   if(cacheDir == ImageFactory::LocalDir) {
0611     imgDir.reset(new ImageDirectory(ImageFactory::localDirectory(localDir_)));
0612   }
0613 
0614   QString id;
0615   StringSet images;
0616   EntryList entries = m_coll->entries();
0617   FieldList imageFields = m_coll->imageFields();
0618   foreach(EntryPtr entry, entries) {
0619     foreach(FieldPtr field, imageFields) {
0620       id = entry->field(field);
0621       if(id.isEmpty() || images.has(id)) {
0622         continue;
0623       }
0624       images.add(id);
0625       if(ImageFactory::imageInfo(id).linkOnly) {
0626         continue;
0627       }
0628       // careful here, if we're writing to LocalDir, need to read from the old LocalDir and write to new
0629       bool success;
0630       if(cacheDir == ImageFactory::LocalDir) {
0631         success = ImageFactory::writeCachedImage(id, imgDir.data());
0632       } else {
0633         success = ImageFactory::writeCachedImage(id, cacheDir);
0634       }
0635       if(!success) {
0636         myLog() << "Failed to write image for entry title:" << entry->title();
0637       }
0638       if(m_cancelImageWriting) {
0639         break;
0640       }
0641     }
0642     if(j%stepSize == 0) {
0643       ProgressManager::self()->setProgress(this, j/stepSize);
0644     }
0645     ++j;
0646     if(m_cancelImageWriting) {
0647       break;
0648     }
0649   }
0650 
0651   if(m_cancelImageWriting) {
0652     myLog() << "Document::writeAllImages() - image writing was cancelled";
0653   }
0654 
0655   m_cancelImageWriting = false;
0656 }
0657 
0658 bool Document::pruneImages() {
0659   bool found = false;
0660   QString id;
0661   StringSet images;
0662   Data::EntryList entries = m_coll->entries();
0663   Data::FieldList imageFields = m_coll->imageFields();
0664   foreach(EntryPtr entry, entries) {
0665     foreach(FieldPtr field, imageFields) {
0666       id = entry->field(field);
0667       if(id.isEmpty() || images.has(id)) {
0668         continue;
0669       }
0670       const Data::Image& img = ImageFactory::imageById(id);
0671       if(img.isNull()) {
0672         entry->setField(field, QString());
0673         found = true;
0674         myDebug() << "removing null image for" << entry->title() << ":" << id;
0675       } else {
0676         images.add(id);
0677       }
0678     }
0679   }
0680   return found;
0681 }
0682 
0683 int Document::imageCount() const {
0684   if(!m_coll) {
0685     return 0;
0686   }
0687   StringSet images;
0688   FieldList fields = m_coll->imageFields();
0689   EntryList entries = m_coll->entries();
0690   foreach(FieldPtr field, fields) {
0691     foreach(EntryPtr entry, entries) {
0692       images.add(entry->field(field));
0693     }
0694   }
0695   return images.count();
0696 }
0697 
0698 void Document::removeImagesNotInCollection(Tellico::Data::EntryList entries_, Tellico::Data::EntryList entriesToKeep_) {
0699   // first get list of all images in collection
0700   StringSet images;
0701   FieldList fields = m_coll->imageFields();
0702   EntryList allEntries = m_coll->entries();
0703   foreach(FieldPtr field, fields) {
0704     foreach(EntryPtr entry, allEntries) {
0705       images.add(entry->field(field));
0706     }
0707     foreach(EntryPtr entry, entriesToKeep_) {
0708       images.add(entry->field(field));
0709     }
0710   }
0711 
0712   // now for all images not in the cache, we can clear them
0713   StringSet imagesToCheck = ImageFactory::imagesNotInCache();
0714 
0715   // if entries_ is not empty, that means we want to limit the images removed
0716   // to those that are referenced in those entries
0717   StringSet imagesToRemove;
0718   foreach(FieldPtr field, fields) {
0719     foreach(EntryPtr entry, entries_) {
0720       QString id = entry->field(field);
0721       if(!id.isEmpty() && imagesToCheck.has(id) && !images.has(id)) {
0722         imagesToRemove.add(id);
0723       }
0724     }
0725   }
0726 
0727   const QStringList realImagesToRemove = imagesToRemove.values();
0728   for(QStringList::ConstIterator it = realImagesToRemove.begin(); it != realImagesToRemove.end(); ++it) {
0729     ImageFactory::removeImage(*it, false); // doesn't delete, just remove link
0730   }
0731 }