File indexing completed on 2024-04-28 04:32:40

0001 /*
0002     SPDX-FileCopyrightText: 2004-2005 Enrico Ros <eros.kde@email.it>
0003     SPDX-FileCopyrightText: 2004-2008 Albert Astals Cid <aacid@kde.org>
0004 
0005     Work sponsored by the LiMux project of the city of Munich:
0006     SPDX-FileCopyrightText: 2017, 2018 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "document.h"
0012 #include "document_p.h"
0013 #include "documentcommands_p.h"
0014 
0015 #include <limits.h>
0016 #include <memory>
0017 #ifdef Q_OS_WIN
0018 #define _WIN32_WINNT 0x0500
0019 #include <windows.h>
0020 #elif defined(Q_OS_FREEBSD)
0021 // clang-format off
0022 // FreeBSD really wants this include order
0023 #include <sys/types.h>
0024 #include <sys/sysctl.h>
0025 // clang-format on
0026 #include <vm/vm_param.h>
0027 #endif
0028 
0029 // qt/kde/system includes
0030 #include <QApplication>
0031 #include <QDesktopServices>
0032 #include <QDir>
0033 #include <QFile>
0034 #include <QFileInfo>
0035 #include <QLabel>
0036 #include <QMap>
0037 #include <QMimeDatabase>
0038 #include <QPageSize>
0039 #include <QPrintDialog>
0040 #include <QRegularExpression>
0041 #include <QScreen>
0042 #include <QStack>
0043 #include <QStandardPaths>
0044 #include <QTemporaryFile>
0045 #include <QTextStream>
0046 #include <QTimer>
0047 #include <QUndoCommand>
0048 #include <QWindow>
0049 #include <QtAlgorithms>
0050 
0051 #include <KApplicationTrader>
0052 #include <KAuthorized>
0053 #include <KConfigDialog>
0054 #include <KFormat>
0055 #include <KIO/Global>
0056 #include <KIO/JobUiDelegate>
0057 #include <KIO/JobUiDelegateFactory>
0058 #include <KIO/OpenUrlJob>
0059 #include <KLocalizedString>
0060 #include <KMacroExpander>
0061 #include <KPluginMetaData>
0062 #include <KProcess>
0063 #include <KShell>
0064 #include <kio_version.h>
0065 #include <kzip.h>
0066 
0067 // local includes
0068 #include "action.h"
0069 #include "annotations.h"
0070 #include "annotations_p.h"
0071 #include "audioplayer.h"
0072 #include "bookmarkmanager.h"
0073 #include "chooseenginedialog_p.h"
0074 #include "debug_p.h"
0075 #include "form.h"
0076 #include "generator_p.h"
0077 #include "interfaces/configinterface.h"
0078 #include "interfaces/guiinterface.h"
0079 #include "interfaces/printinterface.h"
0080 #include "interfaces/saveinterface.h"
0081 #include "misc.h"
0082 #include "observer.h"
0083 #include "page.h"
0084 #include "page_p.h"
0085 #include "pagecontroller_p.h"
0086 #include "script/event_p.h"
0087 #include "scripter.h"
0088 #include "settings_core.h"
0089 #include "sourcereference.h"
0090 #include "sourcereference_p.h"
0091 #include "texteditors_p.h"
0092 #include "tile.h"
0093 #include "tilesmanager_p.h"
0094 #include "utils.h"
0095 #include "utils_p.h"
0096 #include "view.h"
0097 #include "view_p.h"
0098 
0099 #include <config-okular.h>
0100 
0101 #if HAVE_MALLOC_TRIM
0102 #include "malloc.h"
0103 #endif
0104 
0105 using namespace Okular;
0106 
0107 struct AllocatedPixmap {
0108     // owner of the page
0109     DocumentObserver *observer;
0110     int page;
0111     qulonglong memory;
0112     // public constructor: initialize data
0113     AllocatedPixmap(DocumentObserver *o, int p, qulonglong m)
0114         : observer(o)
0115         , page(p)
0116         , memory(m)
0117     {
0118     }
0119 };
0120 
0121 struct ArchiveData {
0122     ArchiveData()
0123     {
0124     }
0125 
0126     QString originalFileName;
0127     QTemporaryFile document;
0128     QTemporaryFile metadataFile;
0129 };
0130 
0131 struct RunningSearch {
0132     // store search properties
0133     int continueOnPage;
0134     RegularAreaRect continueOnMatch;
0135     QSet<int> highlightedPages;
0136 
0137     // fields related to previous searches (used for 'continueSearch')
0138     QString cachedString;
0139     Document::SearchType cachedType;
0140     Qt::CaseSensitivity cachedCaseSensitivity;
0141     bool cachedViewportMove : 1;
0142     bool isCurrentlySearching : 1;
0143     QColor cachedColor;
0144     int pagesDone;
0145 };
0146 
0147 #define foreachObserver(cmd)                                                                                                                                                                                                                   \
0148     {                                                                                                                                                                                                                                          \
0149         QSet<DocumentObserver *>::const_iterator it = d->m_observers.constBegin(), end = d->m_observers.constEnd();                                                                                                                            \
0150         for (; it != end; ++it) {                                                                                                                                                                                                              \
0151             (*it)->cmd;                                                                                                                                                                                                                        \
0152         }                                                                                                                                                                                                                                      \
0153     }
0154 
0155 #define foreachObserverD(cmd)                                                                                                                                                                                                                  \
0156     {                                                                                                                                                                                                                                          \
0157         QSet<DocumentObserver *>::const_iterator it = m_observers.constBegin(), end = m_observers.constEnd();                                                                                                                                  \
0158         for (; it != end; ++it) {                                                                                                                                                                                                              \
0159             (*it)->cmd;                                                                                                                                                                                                                        \
0160         }                                                                                                                                                                                                                                      \
0161     }
0162 
0163 #define OKULAR_HISTORY_MAXSTEPS 100
0164 #define OKULAR_HISTORY_SAVEDSTEPS 10
0165 
0166 // how often to run slotTimedMemoryCheck
0167 constexpr int kMemCheckTime = 2000; // in msec
0168 // getFreeMemory is called every two seconds when checking to see if the system is low on memory. If this timeout was left at kMemCheckTime, half of these checks are useless (when okular is idle) since the cache is used when the cache is
0169 // <=2 seconds old. This means that after the system is out of memory, up to 4 seconds (instead of 2) could go by before okular starts to free memory.
0170 constexpr int kFreeMemCacheTimeout = kMemCheckTime - 100;
0171 
0172 /***** Document ******/
0173 
0174 QString DocumentPrivate::pagesSizeString() const
0175 {
0176     if (m_generator) {
0177         if (m_generator->pagesSizeMetric() != Generator::None) {
0178             QSizeF size = m_parent->allPagesSize();
0179             // Single page size
0180             if (size.isValid()) {
0181                 return localizedSize(size);
0182             }
0183 
0184             // Multiple page sizes
0185             QString sizeString;
0186             QHash<QString, int> pageSizeFrequencies;
0187 
0188             // Compute frequencies of each page size
0189             for (int i = 0; i < m_pagesVector.count(); ++i) {
0190                 const Page *p = m_pagesVector.at(i);
0191                 sizeString = localizedSize(QSizeF(p->width(), p->height()));
0192                 pageSizeFrequencies[sizeString] = pageSizeFrequencies.value(sizeString, 0) + 1;
0193             }
0194 
0195             // Figure out which page size is most frequent
0196             int largestFrequencySeen = 0;
0197             QString mostCommonPageSize = QString();
0198             QHash<QString, int>::const_iterator i = pageSizeFrequencies.constBegin();
0199             while (i != pageSizeFrequencies.constEnd()) {
0200                 if (i.value() > largestFrequencySeen) {
0201                     largestFrequencySeen = i.value();
0202                     mostCommonPageSize = i.key();
0203                 }
0204                 ++i;
0205             }
0206             QString finalText = i18nc("@info %1 is a page size", "Most pages are %1.", mostCommonPageSize);
0207 
0208             return finalText;
0209         } else {
0210             return QString();
0211         }
0212     } else {
0213         return QString();
0214     }
0215 }
0216 
0217 QString DocumentPrivate::namePaperSize(double inchesWidth, double inchesHeight) const
0218 {
0219     const QPageLayout::Orientation orientation = inchesWidth > inchesHeight ? QPageLayout::Landscape : QPageLayout::Portrait;
0220 
0221     const QSize pointsSize(inchesWidth * 72.0, inchesHeight * 72.0);
0222     const QPageSize::PageSizeId paperSize = QPageSize::id(pointsSize, QPageSize::FuzzyOrientationMatch);
0223 
0224     const QString paperName = QPageSize::name(paperSize);
0225 
0226     if (orientation == QPageLayout::Portrait) {
0227         return i18nc("paper type and orientation (eg: Portrait A4)", "Portrait %1", paperName);
0228     } else {
0229         return i18nc("paper type and orientation (eg: Portrait A4)", "Landscape %1", paperName);
0230     }
0231 }
0232 
0233 QString DocumentPrivate::localizedSize(const QSizeF size) const
0234 {
0235     double inchesWidth = 0, inchesHeight = 0;
0236     switch (m_generator->pagesSizeMetric()) {
0237     case Generator::Points:
0238         inchesWidth = size.width() / 72.0;
0239         inchesHeight = size.height() / 72.0;
0240         break;
0241 
0242     case Generator::Pixels: {
0243         const QSizeF dpi = m_generator->dpi();
0244         inchesWidth = size.width() / dpi.width();
0245         inchesHeight = size.height() / dpi.height();
0246     } break;
0247 
0248     case Generator::None:
0249         break;
0250     }
0251     if (QLocale::system().measurementSystem() == QLocale::ImperialSystem) {
0252         return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 in (%3)", inchesWidth, inchesHeight, namePaperSize(inchesWidth, inchesHeight));
0253     } else {
0254         return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 mm (%3)", QString::number(inchesWidth * 25.4, 'd', 0), QString::number(inchesHeight * 25.4, 'd', 0), namePaperSize(inchesWidth, inchesHeight));
0255     }
0256 }
0257 
0258 qulonglong DocumentPrivate::calculateMemoryToFree()
0259 {
0260     // [MEM] choose memory parameters based on configuration profile
0261     qulonglong clipValue = 0;
0262     qulonglong memoryToFree = 0;
0263 
0264     switch (SettingsCore::memoryLevel()) {
0265     case SettingsCore::EnumMemoryLevel::Low:
0266         memoryToFree = m_allocatedPixmapsTotalMemory;
0267         break;
0268 
0269     case SettingsCore::EnumMemoryLevel::Normal: {
0270         qulonglong thirdTotalMemory = getTotalMemory() / 3;
0271         qulonglong freeMemory = getFreeMemory();
0272         if (m_allocatedPixmapsTotalMemory > thirdTotalMemory) {
0273             memoryToFree = m_allocatedPixmapsTotalMemory - thirdTotalMemory;
0274         }
0275         if (m_allocatedPixmapsTotalMemory > freeMemory) {
0276             clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2;
0277         }
0278     } break;
0279 
0280     case SettingsCore::EnumMemoryLevel::Aggressive: {
0281         qulonglong freeMemory = getFreeMemory();
0282         if (m_allocatedPixmapsTotalMemory > freeMemory) {
0283             clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2;
0284         }
0285     } break;
0286     case SettingsCore::EnumMemoryLevel::Greedy: {
0287         qulonglong freeSwap;
0288         qulonglong freeMemory = getFreeMemory(&freeSwap);
0289         const qulonglong memoryLimit = qMin(qMax(freeMemory, getTotalMemory() / 2), freeMemory + freeSwap);
0290         if (m_allocatedPixmapsTotalMemory > memoryLimit) {
0291             clipValue = (m_allocatedPixmapsTotalMemory - memoryLimit) / 2;
0292         }
0293     } break;
0294     }
0295 
0296     if (clipValue > memoryToFree) {
0297         memoryToFree = clipValue;
0298     }
0299 
0300     return memoryToFree;
0301 }
0302 
0303 void DocumentPrivate::cleanupPixmapMemory()
0304 {
0305     cleanupPixmapMemory(calculateMemoryToFree());
0306 }
0307 
0308 void DocumentPrivate::cleanupPixmapMemory(qulonglong memoryToFree)
0309 {
0310     if (memoryToFree < 1) {
0311         return;
0312     }
0313 
0314     const int currentViewportPage = (*m_viewportIterator).pageNumber;
0315 
0316     // Create a QMap of visible rects, indexed by page number
0317     QMap<int, VisiblePageRect *> visibleRects;
0318     QVector<Okular::VisiblePageRect *>::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd();
0319     for (; vIt != vEnd; ++vIt) {
0320         visibleRects.insert((*vIt)->pageNumber, (*vIt));
0321     }
0322 
0323     // Free memory starting from pages that are farthest from the current one
0324     int pagesFreed = 0;
0325     while (memoryToFree > 0) {
0326         AllocatedPixmap *p = searchLowestPriorityPixmap(true, true);
0327         if (!p) { // No pixmap to remove
0328             break;
0329         }
0330 
0331         qCDebug(OkularCoreDebug).nospace() << "Evicting cache pixmap observer=" << p->observer << " page=" << p->page;
0332 
0333         // m_allocatedPixmapsTotalMemory can't underflow because we always add or remove
0334         // the memory used by the AllocatedPixmap so at most it can reach zero
0335         m_allocatedPixmapsTotalMemory -= p->memory;
0336         // Make sure memoryToFree does not underflow
0337         if (p->memory > memoryToFree) {
0338             memoryToFree = 0;
0339         } else {
0340             memoryToFree -= p->memory;
0341         }
0342         pagesFreed++;
0343         // delete pixmap
0344         m_pagesVector.at(p->page)->deletePixmap(p->observer);
0345         // delete allocation descriptor
0346         delete p;
0347     }
0348 
0349     // If we're still on low memory, try to free individual tiles
0350 
0351     // Store pages that weren't completely removed
0352 
0353     std::list<AllocatedPixmap *> pixmapsToKeep;
0354     while (memoryToFree > 0) {
0355         int clean_hits = 0;
0356         for (DocumentObserver *observer : std::as_const(m_observers)) {
0357             AllocatedPixmap *p = searchLowestPriorityPixmap(false, true, observer);
0358             if (!p) { // No pixmap to remove
0359                 continue;
0360             }
0361 
0362             clean_hits++;
0363 
0364             TilesManager *tilesManager = m_pagesVector.at(p->page)->d->tilesManager(observer);
0365             if (tilesManager && tilesManager->totalMemory() > 0) {
0366                 qulonglong memoryDiff = p->memory;
0367                 NormalizedRect visibleRect;
0368                 if (visibleRects.contains(p->page)) {
0369                     visibleRect = visibleRects[p->page]->rect;
0370                 }
0371 
0372                 // Free non visible tiles
0373                 tilesManager->cleanupPixmapMemory(memoryToFree, visibleRect, currentViewportPage);
0374 
0375                 p->memory = tilesManager->totalMemory();
0376                 memoryDiff -= p->memory;
0377                 memoryToFree = (memoryDiff < memoryToFree) ? (memoryToFree - memoryDiff) : 0;
0378                 m_allocatedPixmapsTotalMemory -= memoryDiff;
0379 
0380                 if (p->memory > 0) {
0381                     pixmapsToKeep.push_back(p);
0382                 } else {
0383                     delete p;
0384                 }
0385             } else {
0386                 pixmapsToKeep.push_back(p);
0387             }
0388         }
0389 
0390         if (clean_hits == 0) {
0391             break;
0392         }
0393     }
0394 
0395     m_allocatedPixmaps.splice(m_allocatedPixmaps.end(), pixmapsToKeep);
0396     Q_UNUSED(pagesFreed);
0397     // p--rintf("freeMemory A:[%d -%d = %d] \n", m_allocatedPixmaps.count() + pagesFreed, pagesFreed, m_allocatedPixmaps.count() );
0398 }
0399 
0400 /* Returns the next pixmap to evict from cache, or NULL if no suitable pixmap
0401  * if found. If unloadableOnly is set, only unloadable pixmaps are returned. If
0402  * thenRemoveIt is set, the pixmap is removed from m_allocatedPixmaps before
0403  * returning it
0404  */
0405 AllocatedPixmap *DocumentPrivate::searchLowestPriorityPixmap(bool unloadableOnly, bool thenRemoveIt, DocumentObserver *observer)
0406 {
0407     std::list<AllocatedPixmap *>::iterator pIt = m_allocatedPixmaps.begin();
0408     std::list<AllocatedPixmap *>::iterator pEnd = m_allocatedPixmaps.end();
0409     std::list<AllocatedPixmap *>::iterator farthestPixmap = pEnd;
0410     const int currentViewportPage = (*m_viewportIterator).pageNumber;
0411 
0412     /* Find the pixmap that is farthest from the current viewport */
0413     int maxDistance = -1;
0414     while (pIt != pEnd) {
0415         const AllocatedPixmap *p = *pIt;
0416         // Filter by observer
0417         if (observer == nullptr || p->observer == observer) {
0418             const int distance = qAbs(p->page - currentViewportPage);
0419             if (maxDistance < distance && (!unloadableOnly || p->observer->canUnloadPixmap(p->page))) {
0420                 maxDistance = distance;
0421                 farthestPixmap = pIt;
0422             }
0423         }
0424         ++pIt;
0425     }
0426 
0427     /* No pixmap to remove */
0428     if (farthestPixmap == pEnd) {
0429         return nullptr;
0430     }
0431 
0432     AllocatedPixmap *selectedPixmap = *farthestPixmap;
0433     if (thenRemoveIt) {
0434         m_allocatedPixmaps.erase(farthestPixmap);
0435     }
0436     return selectedPixmap;
0437 }
0438 
0439 qulonglong DocumentPrivate::getTotalMemory()
0440 {
0441     static qulonglong cachedValue = 0;
0442     if (cachedValue) {
0443         return cachedValue;
0444     }
0445 
0446 #if defined(Q_OS_LINUX)
0447     // if /proc/meminfo doesn't exist, return 128MB
0448     QFile memFile(QStringLiteral("/proc/meminfo"));
0449     if (!memFile.open(QIODevice::ReadOnly)) {
0450         return (cachedValue = 134217728);
0451     }
0452 
0453     QTextStream readStream(&memFile);
0454     while (true) {
0455         QString entry = readStream.readLine();
0456         if (entry.isNull()) {
0457             break;
0458         }
0459         if (entry.startsWith(QLatin1String("MemTotal:"))) {
0460             return (cachedValue = (Q_UINT64_C(1024) * entry.section(QLatin1Char(' '), -2, -2).toULongLong()));
0461         }
0462     }
0463 #elif defined(Q_OS_FREEBSD)
0464     qulonglong physmem;
0465     int mib[] = {CTL_HW, HW_PHYSMEM};
0466     size_t len = sizeof(physmem);
0467     if (sysctl(mib, 2, &physmem, &len, NULL, 0) == 0)
0468         return (cachedValue = physmem);
0469 #elif defined(Q_OS_WIN)
0470     MEMORYSTATUSEX stat;
0471     stat.dwLength = sizeof(stat);
0472     GlobalMemoryStatusEx(&stat);
0473 
0474     return (cachedValue = stat.ullTotalPhys);
0475 #endif
0476     return (cachedValue = 134217728);
0477 }
0478 
0479 qulonglong DocumentPrivate::getFreeMemory(qulonglong *freeSwap)
0480 {
0481     static QDeadlineTimer cacheTimer(0);
0482     static qulonglong cachedValue = 0;
0483     static qulonglong cachedFreeSwap = 0;
0484 
0485     if (!cacheTimer.hasExpired()) {
0486         if (freeSwap) {
0487             *freeSwap = cachedFreeSwap;
0488         }
0489         return cachedValue;
0490     }
0491 
0492     /* Initialize the returned free swap value to 0. It is overwritten if the
0493      * actual value is available */
0494     if (freeSwap) {
0495         *freeSwap = 0;
0496     }
0497 
0498 #if defined(Q_OS_LINUX)
0499     // if /proc/meminfo doesn't exist, return MEMORY FULL
0500     QFile memFile(QStringLiteral("/proc/meminfo"));
0501     if (!memFile.open(QIODevice::ReadOnly)) {
0502         return 0;
0503     }
0504 
0505     // read /proc/meminfo and sum up the contents of 'MemFree', 'Buffers'
0506     // and 'Cached' fields. consider swapped memory as used memory.
0507     qulonglong memoryFree = 0;
0508     QString entry;
0509     QTextStream readStream(&memFile);
0510     static const int nElems = 5;
0511     QString names[nElems] = {QStringLiteral("MemFree:"), QStringLiteral("Buffers:"), QStringLiteral("Cached:"), QStringLiteral("SwapFree:"), QStringLiteral("SwapTotal:")};
0512     qulonglong values[nElems] = {0, 0, 0, 0, 0};
0513     bool foundValues[nElems] = {false, false, false, false, false};
0514     while (true) {
0515         entry = readStream.readLine();
0516         if (entry.isNull()) {
0517             break;
0518         }
0519         for (int i = 0; i < nElems; ++i) {
0520             if (entry.startsWith(names[i])) {
0521                 values[i] = entry.section(QLatin1Char(' '), -2, -2).toULongLong(&foundValues[i]);
0522             }
0523         }
0524     }
0525     memFile.close();
0526     bool found = true;
0527     for (int i = 0; found && i < nElems; ++i) {
0528         found = found && foundValues[i];
0529     }
0530     if (found) {
0531         /* MemFree + Buffers + Cached - SwapUsed =
0532          * = MemFree + Buffers + Cached - (SwapTotal - SwapFree) =
0533          * = MemFree + Buffers + Cached + SwapFree - SwapTotal */
0534         memoryFree = values[0] + values[1] + values[2] + values[3];
0535         if (values[4] > memoryFree) {
0536             memoryFree = 0;
0537         } else {
0538             memoryFree -= values[4];
0539         }
0540     } else {
0541         return 0;
0542     }
0543 
0544     cacheTimer.setRemainingTime(kFreeMemCacheTimeout);
0545 
0546     if (freeSwap) {
0547         *freeSwap = (cachedFreeSwap = (Q_UINT64_C(1024) * values[3]));
0548     }
0549     return (cachedValue = (Q_UINT64_C(1024) * memoryFree));
0550 #elif defined(Q_OS_FREEBSD)
0551     qulonglong cache, inact, free, psize;
0552     size_t cachelen, inactlen, freelen, psizelen;
0553     cachelen = sizeof(cache);
0554     inactlen = sizeof(inact);
0555     freelen = sizeof(free);
0556     psizelen = sizeof(psize);
0557     // sum up inactive, cached and free memory
0558     if (sysctlbyname("vm.stats.vm.v_cache_count", &cache, &cachelen, NULL, 0) == 0 && sysctlbyname("vm.stats.vm.v_inactive_count", &inact, &inactlen, NULL, 0) == 0 &&
0559         sysctlbyname("vm.stats.vm.v_free_count", &free, &freelen, NULL, 0) == 0 && sysctlbyname("vm.stats.vm.v_page_size", &psize, &psizelen, NULL, 0) == 0) {
0560         cacheTimer.setRemainingTime(kFreeMemCacheTimeout);
0561         return (cachedValue = (cache + inact + free) * psize);
0562     } else {
0563         return 0;
0564     }
0565 #elif defined(Q_OS_WIN)
0566     MEMORYSTATUSEX stat;
0567     stat.dwLength = sizeof(stat);
0568     GlobalMemoryStatusEx(&stat);
0569 
0570     cacheTimer.setRemainingTime(kFreeMemCacheTimeout);
0571 
0572     if (freeSwap)
0573         *freeSwap = (cachedFreeSwap = stat.ullAvailPageFile);
0574     return (cachedValue = stat.ullAvailPhys);
0575 #else
0576     // tell the memory is full.. will act as in LOW profile
0577     return 0;
0578 #endif
0579 }
0580 
0581 bool DocumentPrivate::loadDocumentInfo(LoadDocumentInfoFlags loadWhat)
0582 // note: load data and stores it internally (document or pages). observers
0583 // are still uninitialized at this point so don't access them
0584 {
0585     // qCDebug(OkularCoreDebug).nospace() << "Using '" << d->m_xmlFileName << "' as document info file.";
0586     if (m_xmlFileName.isEmpty()) {
0587         return false;
0588     }
0589 
0590     QFile infoFile(m_xmlFileName);
0591     return loadDocumentInfo(infoFile, loadWhat);
0592 }
0593 
0594 bool DocumentPrivate::loadDocumentInfo(QFile &infoFile, LoadDocumentInfoFlags loadWhat)
0595 {
0596     if (!infoFile.exists() || !infoFile.open(QIODevice::ReadOnly)) {
0597         return false;
0598     }
0599 
0600     // Load DOM from XML file
0601     QDomDocument doc(QStringLiteral("documentInfo"));
0602     if (!doc.setContent(&infoFile)) {
0603         qCDebug(OkularCoreDebug) << "Can't load XML pair! Check for broken xml.";
0604         infoFile.close();
0605         return false;
0606     }
0607     infoFile.close();
0608 
0609     QDomElement root = doc.documentElement();
0610 
0611     if (root.tagName() != QLatin1String("documentInfo")) {
0612         return false;
0613     }
0614 
0615     bool loadedAnything = false; // set if something gets actually loaded
0616 
0617     // Parse the DOM tree
0618     QDomNode topLevelNode = root.firstChild();
0619     while (topLevelNode.isElement()) {
0620         QString catName = topLevelNode.toElement().tagName();
0621 
0622         // Restore page attributes (bookmark, annotations, ...) from the DOM
0623         if (catName == QLatin1String("pageList") && (loadWhat & LoadPageInfo)) {
0624             QDomNode pageNode = topLevelNode.firstChild();
0625             while (pageNode.isElement()) {
0626                 QDomElement pageElement = pageNode.toElement();
0627                 if (pageElement.hasAttribute(QStringLiteral("number"))) {
0628                     // get page number (node's attribute)
0629                     bool ok;
0630                     int pageNumber = pageElement.attribute(QStringLiteral("number")).toInt(&ok);
0631 
0632                     // pass the domElement to the right page, to read config data from
0633                     if (ok && pageNumber >= 0 && pageNumber < (int)m_pagesVector.count()) {
0634                         if (m_pagesVector[pageNumber]->d->restoreLocalContents(pageElement)) {
0635                             loadedAnything = true;
0636                         }
0637                     }
0638                 }
0639                 pageNode = pageNode.nextSibling();
0640             }
0641         }
0642 
0643         // Restore 'general info' from the DOM
0644         else if (catName == QLatin1String("generalInfo") && (loadWhat & LoadGeneralInfo)) {
0645             QDomNode infoNode = topLevelNode.firstChild();
0646             while (infoNode.isElement()) {
0647                 QDomElement infoElement = infoNode.toElement();
0648 
0649                 // restore viewports history
0650                 if (infoElement.tagName() == QLatin1String("history")) {
0651                     // clear history
0652                     m_viewportHistory.clear();
0653                     // append old viewports
0654                     QDomNode historyNode = infoNode.firstChild();
0655                     while (historyNode.isElement()) {
0656                         QDomElement historyElement = historyNode.toElement();
0657                         if (historyElement.hasAttribute(QStringLiteral("viewport"))) {
0658                             QString vpString = historyElement.attribute(QStringLiteral("viewport"));
0659                             m_viewportIterator = m_viewportHistory.insert(m_viewportHistory.end(), DocumentViewport(vpString));
0660                             loadedAnything = true;
0661                         }
0662                         historyNode = historyNode.nextSibling();
0663                     }
0664                     // consistency check
0665                     if (m_viewportHistory.empty()) {
0666                         m_viewportIterator = m_viewportHistory.insert(m_viewportHistory.end(), DocumentViewport());
0667                     }
0668                 } else if (infoElement.tagName() == QLatin1String("rotation")) {
0669                     QString str = infoElement.text();
0670                     bool ok = true;
0671                     int newrotation = !str.isEmpty() ? (str.toInt(&ok) % 4) : 0;
0672                     if (ok && newrotation != 0) {
0673                         setRotationInternal(newrotation, false);
0674                         loadedAnything = true;
0675                     }
0676                 } else if (infoElement.tagName() == QLatin1String("views")) {
0677                     QDomNode viewNode = infoNode.firstChild();
0678                     while (viewNode.isElement()) {
0679                         QDomElement viewElement = viewNode.toElement();
0680                         if (viewElement.tagName() == QLatin1String("view")) {
0681                             const QString viewName = viewElement.attribute(QStringLiteral("name"));
0682                             for (View *view : std::as_const(m_views)) {
0683                                 if (view->name() == viewName) {
0684                                     loadViewsInfo(view, viewElement);
0685                                     loadedAnything = true;
0686                                     break;
0687                                 }
0688                             }
0689                         }
0690                         viewNode = viewNode.nextSibling();
0691                     }
0692                 }
0693                 infoNode = infoNode.nextSibling();
0694             }
0695         }
0696 
0697         topLevelNode = topLevelNode.nextSibling();
0698     } // </documentInfo>
0699 
0700     return loadedAnything;
0701 }
0702 
0703 void DocumentPrivate::loadViewsInfo(View *view, const QDomElement &e)
0704 {
0705     QDomNode viewNode = e.firstChild();
0706     while (viewNode.isElement()) {
0707         QDomElement viewElement = viewNode.toElement();
0708 
0709         if (viewElement.tagName() == QLatin1String("zoom")) {
0710             const QString valueString = viewElement.attribute(QStringLiteral("value"));
0711             bool newzoom_ok = true;
0712             const double newzoom = !valueString.isEmpty() ? valueString.toDouble(&newzoom_ok) : 1.0;
0713             if (newzoom_ok && newzoom != 0 && view->supportsCapability(View::Zoom) && (view->capabilityFlags(View::Zoom) & (View::CapabilityRead | View::CapabilitySerializable))) {
0714                 view->setCapability(View::Zoom, newzoom);
0715             }
0716             const QString modeString = viewElement.attribute(QStringLiteral("mode"));
0717             bool newmode_ok = true;
0718             const int newmode = !modeString.isEmpty() ? modeString.toInt(&newmode_ok) : 2;
0719             if (newmode_ok && view->supportsCapability(View::ZoomModality) && (view->capabilityFlags(View::ZoomModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
0720                 view->setCapability(View::ZoomModality, newmode);
0721             }
0722         } else if (viewElement.tagName() == QLatin1String("viewMode")) {
0723             const QString modeString = viewElement.attribute(QStringLiteral("mode"));
0724             bool newmode_ok = true;
0725             const int newmode = !modeString.isEmpty() ? modeString.toInt(&newmode_ok) : 2;
0726             if (newmode_ok && view->supportsCapability(View::ViewModeModality) && (view->capabilityFlags(View::ViewModeModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
0727                 view->setCapability(View::ViewModeModality, newmode);
0728             }
0729         } else if (viewElement.tagName() == QLatin1String("continuous")) {
0730             const QString modeString = viewElement.attribute(QStringLiteral("mode"));
0731             bool newmode_ok = true;
0732             const int newmode = !modeString.isEmpty() ? modeString.toInt(&newmode_ok) : 2;
0733             if (newmode_ok && view->supportsCapability(View::Continuous) && (view->capabilityFlags(View::Continuous) & (View::CapabilityRead | View::CapabilitySerializable))) {
0734                 view->setCapability(View::Continuous, newmode);
0735             }
0736         } else if (viewElement.tagName() == QLatin1String("trimMargins")) {
0737             const QString valueString = viewElement.attribute(QStringLiteral("value"));
0738             bool newmode_ok = true;
0739             const int newmode = !valueString.isEmpty() ? valueString.toInt(&newmode_ok) : 2;
0740             if (newmode_ok && view->supportsCapability(View::TrimMargins) && (view->capabilityFlags(View::TrimMargins) & (View::CapabilityRead | View::CapabilitySerializable))) {
0741                 view->setCapability(View::TrimMargins, newmode);
0742             }
0743         }
0744 
0745         viewNode = viewNode.nextSibling();
0746     }
0747 }
0748 
0749 void DocumentPrivate::saveViewsInfo(View *view, QDomElement &e) const
0750 {
0751     if (view->supportsCapability(View::Zoom) && (view->capabilityFlags(View::Zoom) & (View::CapabilityRead | View::CapabilitySerializable)) && view->supportsCapability(View::ZoomModality) &&
0752         (view->capabilityFlags(View::ZoomModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
0753         QDomElement zoomEl = e.ownerDocument().createElement(QStringLiteral("zoom"));
0754         e.appendChild(zoomEl);
0755         bool ok = true;
0756         const double zoom = view->capability(View::Zoom).toDouble(&ok);
0757         if (ok && zoom != 0) {
0758             zoomEl.setAttribute(QStringLiteral("value"), QString::number(zoom));
0759         }
0760         const int mode = view->capability(View::ZoomModality).toInt(&ok);
0761         if (ok) {
0762             zoomEl.setAttribute(QStringLiteral("mode"), mode);
0763         }
0764     }
0765     if (view->supportsCapability(View::Continuous) && (view->capabilityFlags(View::Continuous) & (View::CapabilityRead | View::CapabilitySerializable))) {
0766         QDomElement contEl = e.ownerDocument().createElement(QStringLiteral("continuous"));
0767         e.appendChild(contEl);
0768         const bool mode = view->capability(View::Continuous).toBool();
0769         contEl.setAttribute(QStringLiteral("mode"), mode);
0770     }
0771     if (view->supportsCapability(View::ViewModeModality) && (view->capabilityFlags(View::ViewModeModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
0772         QDomElement viewEl = e.ownerDocument().createElement(QStringLiteral("viewMode"));
0773         e.appendChild(viewEl);
0774         bool ok = true;
0775         const int mode = view->capability(View::ViewModeModality).toInt(&ok);
0776         if (ok) {
0777             viewEl.setAttribute(QStringLiteral("mode"), mode);
0778         }
0779     }
0780     if (view->supportsCapability(View::TrimMargins) && (view->capabilityFlags(View::TrimMargins) & (View::CapabilityRead | View::CapabilitySerializable))) {
0781         QDomElement contEl = e.ownerDocument().createElement(QStringLiteral("trimMargins"));
0782         e.appendChild(contEl);
0783         const bool value = view->capability(View::TrimMargins).toBool();
0784         contEl.setAttribute(QStringLiteral("value"), value);
0785     }
0786 }
0787 
0788 QUrl DocumentPrivate::giveAbsoluteUrl(const QString &fileName) const
0789 {
0790     if (!QDir::isRelativePath(fileName)) {
0791         return QUrl::fromLocalFile(fileName);
0792     }
0793 
0794     if (!m_url.isValid()) {
0795         return QUrl();
0796     }
0797 
0798     return QUrl(KIO::upUrl(m_url).toString() + fileName);
0799 }
0800 
0801 bool DocumentPrivate::openRelativeFile(const QString &fileName)
0802 {
0803     const QUrl newUrl = giveAbsoluteUrl(fileName);
0804     if (newUrl.isEmpty()) {
0805         return false;
0806     }
0807 
0808     qCDebug(OkularCoreDebug).nospace() << "openRelativeFile: '" << newUrl << "'";
0809 
0810     Q_EMIT m_parent->openUrl(newUrl);
0811     return m_url == newUrl;
0812 }
0813 
0814 Generator *DocumentPrivate::loadGeneratorLibrary(const KPluginMetaData &service)
0815 {
0816     const auto result = KPluginFactory::instantiatePlugin<Okular::Generator>(service);
0817 
0818     if (!result) {
0819         qCWarning(OkularCoreDebug).nospace() << "Failed to load plugin " << service.fileName() << ": " << result.errorText;
0820         return nullptr;
0821     }
0822 
0823     GeneratorInfo info(result.plugin, service);
0824     m_loadedGenerators.insert(service.pluginId(), info);
0825     return result.plugin;
0826 }
0827 
0828 void DocumentPrivate::loadAllGeneratorLibraries()
0829 {
0830     if (m_generatorsLoaded) {
0831         return;
0832     }
0833 
0834     loadServiceList(availableGenerators());
0835 
0836     m_generatorsLoaded = true;
0837 }
0838 
0839 void DocumentPrivate::loadServiceList(const QVector<KPluginMetaData> &offers)
0840 {
0841     int count = offers.count();
0842     if (count <= 0) {
0843         return;
0844     }
0845 
0846     for (int i = 0; i < count; ++i) {
0847         QString id = offers.at(i).pluginId();
0848         // don't load already loaded generators
0849         QHash<QString, GeneratorInfo>::const_iterator genIt = m_loadedGenerators.constFind(id);
0850         if (!m_loadedGenerators.isEmpty() && genIt != m_loadedGenerators.constEnd()) {
0851             continue;
0852         }
0853 
0854         Generator *g = loadGeneratorLibrary(offers.at(i));
0855         (void)g;
0856     }
0857 }
0858 
0859 void DocumentPrivate::unloadGenerator(const GeneratorInfo &info)
0860 {
0861     delete info.generator;
0862 }
0863 
0864 void DocumentPrivate::cacheExportFormats()
0865 {
0866     if (m_exportCached) {
0867         return;
0868     }
0869 
0870     const ExportFormat::List formats = m_generator->exportFormats();
0871     for (int i = 0; i < formats.count(); ++i) {
0872         if (formats.at(i).mimeType().name() == QLatin1String("text/plain")) {
0873             m_exportToText = formats.at(i);
0874         } else {
0875             m_exportFormats.append(formats.at(i));
0876         }
0877     }
0878 
0879     m_exportCached = true;
0880 }
0881 
0882 ConfigInterface *DocumentPrivate::generatorConfig(GeneratorInfo &info)
0883 {
0884     if (info.configChecked) {
0885         return info.config;
0886     }
0887 
0888     info.config = qobject_cast<Okular::ConfigInterface *>(info.generator);
0889     info.configChecked = true;
0890     return info.config;
0891 }
0892 
0893 SaveInterface *DocumentPrivate::generatorSave(GeneratorInfo &info)
0894 {
0895     if (info.saveChecked) {
0896         return info.save;
0897     }
0898 
0899     info.save = qobject_cast<Okular::SaveInterface *>(info.generator);
0900     info.saveChecked = true;
0901     return info.save;
0902 }
0903 
0904 Document::OpenResult DocumentPrivate::openDocumentInternal(const KPluginMetaData &offer, bool isstdin, const QString &docFile, const QByteArray &filedata, const QString &password)
0905 {
0906     QString propName = offer.pluginId();
0907     QHash<QString, GeneratorInfo>::const_iterator genIt = m_loadedGenerators.constFind(propName);
0908     m_walletGenerator = nullptr;
0909     if (genIt != m_loadedGenerators.constEnd()) {
0910         m_generator = genIt.value().generator;
0911     } else {
0912         m_generator = loadGeneratorLibrary(offer);
0913         if (!m_generator) {
0914             return Document::OpenError;
0915         }
0916         genIt = m_loadedGenerators.constFind(propName);
0917         Q_ASSERT(genIt != m_loadedGenerators.constEnd());
0918     }
0919     Q_ASSERT_X(m_generator, "Document::load()", "null generator?!");
0920 
0921     m_generator->d_func()->m_document = this;
0922 
0923     // connect error reporting signals
0924     m_openError.clear();
0925     QMetaObject::Connection errorToOpenErrorConnection = QObject::connect(m_generator, &Generator::error, m_parent, [this](const QString &message) { m_openError = message; });
0926     QObject::connect(m_generator, &Generator::warning, m_parent, &Document::warning);
0927     QObject::connect(m_generator, &Generator::notice, m_parent, &Document::notice);
0928 
0929     QApplication::setOverrideCursor(Qt::WaitCursor);
0930 
0931     const QWindow *window = m_widget && m_widget->window() ? m_widget->window()->windowHandle() : nullptr;
0932     const QSizeF dpi = Utils::realDpi(window);
0933     qCDebug(OkularCoreDebug) << "Output DPI:" << dpi;
0934     m_generator->setDPI(dpi);
0935 
0936     Document::OpenResult openResult = Document::OpenError;
0937     if (!isstdin) {
0938         openResult = m_generator->loadDocumentWithPassword(docFile, m_pagesVector, password);
0939     } else if (!filedata.isEmpty()) {
0940         if (m_generator->hasFeature(Generator::ReadRawData)) {
0941             openResult = m_generator->loadDocumentFromDataWithPassword(filedata, m_pagesVector, password);
0942         } else {
0943             m_tempFile = new QTemporaryFile();
0944             if (!m_tempFile->open()) {
0945                 delete m_tempFile;
0946                 m_tempFile = nullptr;
0947             } else {
0948                 m_tempFile->write(filedata);
0949                 QString tmpFileName = m_tempFile->fileName();
0950                 m_tempFile->close();
0951                 openResult = m_generator->loadDocumentWithPassword(tmpFileName, m_pagesVector, password);
0952             }
0953         }
0954     }
0955 
0956     QApplication::restoreOverrideCursor();
0957     if (openResult != Document::OpenSuccess || m_pagesVector.size() <= 0) {
0958         m_generator->d_func()->m_document = nullptr;
0959         QObject::disconnect(m_generator, nullptr, m_parent, nullptr);
0960 
0961         // TODO this is a bit of a hack, since basically means that
0962         // you can only call walletDataForFile after calling openDocument
0963         // but since in reality it's what happens I've decided not to refactor/break API
0964         // One solution is just kill walletDataForFile and make OpenResult be an object
0965         // where the wallet data is also returned when OpenNeedsPassword
0966         m_walletGenerator = m_generator;
0967         m_generator = nullptr;
0968 
0969         qDeleteAll(m_pagesVector);
0970         m_pagesVector.clear();
0971         delete m_tempFile;
0972         m_tempFile = nullptr;
0973 
0974         // TODO: Q_EMIT a message telling the document is empty
0975         if (openResult == Document::OpenSuccess) {
0976             openResult = Document::OpenError;
0977         }
0978     } else {
0979         /*
0980          *  Now that the documen is opened, the tab (if using tabs) is visible, which mean that
0981          * we can now connect the error reporting signal directly to the parent
0982          */
0983 
0984         QObject::disconnect(errorToOpenErrorConnection);
0985         QObject::connect(m_generator, &Generator::error, m_parent, &Document::error);
0986     }
0987 
0988     return openResult;
0989 }
0990 
0991 bool DocumentPrivate::savePageDocumentInfo(QTemporaryFile *infoFile, int what) const
0992 {
0993     if (infoFile->open()) {
0994         // 1. Create DOM
0995         QDomDocument doc(QStringLiteral("documentInfo"));
0996         QDomProcessingInstruction xmlPi = doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
0997         doc.appendChild(xmlPi);
0998         QDomElement root = doc.createElement(QStringLiteral("documentInfo"));
0999         doc.appendChild(root);
1000 
1001         // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM
1002         QDomElement pageList = doc.createElement(QStringLiteral("pageList"));
1003         root.appendChild(pageList);
1004         // <page list><page number='x'>.... </page> save pages that hold data
1005         QVector<Page *>::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd();
1006         for (; pIt != pEnd; ++pIt) {
1007             (*pIt)->d->saveLocalContents(pageList, doc, PageItems(what));
1008         }
1009 
1010         // 3. Save DOM to XML file
1011         QString xml = doc.toString();
1012 
1013         QTextStream os(infoFile);
1014         os.setEncoding(QStringConverter::Utf8);
1015         os << xml;
1016         return true;
1017     }
1018     return false;
1019 }
1020 
1021 DocumentViewport DocumentPrivate::nextDocumentViewport() const
1022 {
1023     DocumentViewport ret = m_nextDocumentViewport;
1024     if (!m_nextDocumentDestination.isEmpty() && m_generator) {
1025         DocumentViewport vp(m_parent->metaData(QStringLiteral("NamedViewport"), m_nextDocumentDestination).toString());
1026         if (vp.isValid()) {
1027             ret = vp;
1028         }
1029     }
1030     return ret;
1031 }
1032 
1033 void DocumentPrivate::performAddPageAnnotation(int page, Annotation *annotation)
1034 {
1035     Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
1036     AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr;
1037 
1038     // find out the page to attach annotation
1039     Page *kp = m_pagesVector[page];
1040     if (!m_generator || !kp) {
1041         return;
1042     }
1043 
1044     // the annotation belongs already to a page
1045     if (annotation->d_ptr->m_page) {
1046         return;
1047     }
1048 
1049     // add annotation to the page
1050     kp->addAnnotation(annotation);
1051 
1052     // tell the annotation proxy
1053     if (proxy && proxy->supports(AnnotationProxy::Addition)) {
1054         proxy->notifyAddition(annotation, page);
1055     }
1056 
1057     // notify observers about the change
1058     notifyAnnotationChanges(page);
1059 
1060     if (annotation->flags() & Annotation::ExternallyDrawn) {
1061         // Redraw everything, including ExternallyDrawn annotations
1062         refreshPixmaps(page);
1063     }
1064 }
1065 
1066 void DocumentPrivate::performRemovePageAnnotation(int page, Annotation *annotation)
1067 {
1068     Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
1069     AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr;
1070     bool isExternallyDrawn;
1071 
1072     // find out the page
1073     Page *kp = m_pagesVector[page];
1074     if (!m_generator || !kp) {
1075         return;
1076     }
1077 
1078     if (annotation->flags() & Annotation::ExternallyDrawn) {
1079         isExternallyDrawn = true;
1080     } else {
1081         isExternallyDrawn = false;
1082     }
1083 
1084     // try to remove the annotation
1085     if (m_parent->canRemovePageAnnotation(annotation)) {
1086         // tell the annotation proxy
1087         if (proxy && proxy->supports(AnnotationProxy::Removal)) {
1088             proxy->notifyRemoval(annotation, page);
1089         }
1090 
1091         kp->removeAnnotation(annotation); // Also destroys the object
1092 
1093         // in case of success, notify observers about the change
1094         notifyAnnotationChanges(page);
1095 
1096         if (isExternallyDrawn) {
1097             // Redraw everything, including ExternallyDrawn annotations
1098             refreshPixmaps(page);
1099         }
1100     }
1101 }
1102 
1103 void DocumentPrivate::performModifyPageAnnotation(int page, Annotation *annotation, bool appearanceChanged)
1104 {
1105     Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
1106     AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr;
1107 
1108     // find out the page
1109     Page *kp = m_pagesVector[page];
1110     if (!m_generator || !kp) {
1111         return;
1112     }
1113 
1114     // tell the annotation proxy
1115     if (proxy && proxy->supports(AnnotationProxy::Modification)) {
1116         proxy->notifyModification(annotation, page, appearanceChanged);
1117     }
1118 
1119     // notify observers about the change
1120     notifyAnnotationChanges(page);
1121     if (appearanceChanged && (annotation->flags() & Annotation::ExternallyDrawn)) {
1122         /* When an annotation is being moved, the generator will not render it.
1123          * Therefore there's no need to refresh pixmaps after the first time */
1124         if (annotation->flags() & (Annotation::BeingMoved | Annotation::BeingResized)) {
1125             if (m_annotationBeingModified) {
1126                 return;
1127             } else { // First time: take note
1128                 m_annotationBeingModified = true;
1129             }
1130         } else {
1131             m_annotationBeingModified = false;
1132         }
1133 
1134         // Redraw everything, including ExternallyDrawn annotations
1135         qCDebug(OkularCoreDebug) << "Refreshing Pixmaps";
1136         refreshPixmaps(page);
1137     }
1138 }
1139 
1140 void DocumentPrivate::performSetAnnotationContents(const QString &newContents, Annotation *annot, int pageNumber)
1141 {
1142     bool appearanceChanged = false;
1143 
1144     // Check if appearanceChanged should be true
1145     switch (annot->subType()) {
1146     // If it's an in-place TextAnnotation, set the inplace text
1147     case Okular::Annotation::AText: {
1148         Okular::TextAnnotation *txtann = static_cast<Okular::TextAnnotation *>(annot);
1149         if (txtann->textType() == Okular::TextAnnotation::InPlace) {
1150             appearanceChanged = true;
1151         }
1152         break;
1153     }
1154     // If it's a LineAnnotation, check if caption text is visible
1155     case Okular::Annotation::ALine: {
1156         Okular::LineAnnotation *lineann = static_cast<Okular::LineAnnotation *>(annot);
1157         if (lineann->showCaption()) {
1158             appearanceChanged = true;
1159         }
1160         break;
1161     }
1162     default:
1163         break;
1164     }
1165 
1166     // Set contents
1167     annot->setContents(newContents);
1168 
1169     // Tell the document the annotation has been modified
1170     performModifyPageAnnotation(pageNumber, annot, appearanceChanged);
1171 }
1172 
1173 void DocumentPrivate::recalculateForms()
1174 {
1175     const QVariant fco = m_parent->metaData(QStringLiteral("FormCalculateOrder"));
1176     const QVector<int> formCalculateOrder = fco.value<QVector<int>>();
1177     for (int formId : formCalculateOrder) {
1178         for (uint pageIdx = 0; pageIdx < m_parent->pages(); pageIdx++) {
1179             const Page *p = m_parent->page(pageIdx);
1180             if (p) {
1181                 bool pageNeedsRefresh = false;
1182                 const QList<Okular::FormField *> forms = p->formFields();
1183                 for (FormField *form : forms) {
1184                     if (form->id() == formId) {
1185                         Action *action = form->additionalAction(FormField::CalculateField);
1186                         if (action) {
1187                             FormFieldText *fft = dynamic_cast<FormFieldText *>(form);
1188                             std::shared_ptr<Event> event;
1189                             QString oldVal;
1190                             if (fft) {
1191                                 // Prepare text calculate event
1192                                 event = Event::createFormCalculateEvent(fft, m_pagesVector[pageIdx]);
1193                                 if (!m_scripter) {
1194                                     m_scripter = new Scripter(this);
1195                                 }
1196                                 m_scripter->setEvent(event.get());
1197                                 // The value maybe changed in javascript so save it first.
1198                                 oldVal = fft->text();
1199                             }
1200 
1201                             m_parent->processAction(action);
1202                             if (event && fft) {
1203                                 // Update text field from calculate
1204                                 m_scripter->setEvent(nullptr);
1205                                 const QString newVal = event->value().toString();
1206                                 if (newVal != oldVal) {
1207                                     fft->setText(newVal);
1208                                     fft->setAppearanceText(newVal);
1209                                     if (const Okular::Action *action = fft->additionalAction(Okular::FormField::FormatField)) {
1210                                         // The format action handles the refresh.
1211                                         m_parent->processFormatAction(action, fft);
1212                                     } else {
1213                                         Q_EMIT m_parent->refreshFormWidget(fft);
1214                                         pageNeedsRefresh = true;
1215                                     }
1216                                 }
1217                             }
1218                         } else {
1219                             qWarning() << "Form that is part of calculate order doesn't have a calculate action";
1220                         }
1221                     }
1222                 }
1223                 if (pageNeedsRefresh) {
1224                     refreshPixmaps(p->number());
1225                 }
1226             }
1227         }
1228     }
1229 }
1230 
1231 void DocumentPrivate::saveDocumentInfo() const
1232 {
1233     if (m_xmlFileName.isEmpty()) {
1234         return;
1235     }
1236 
1237     QFile infoFile(m_xmlFileName);
1238     qCDebug(OkularCoreDebug) << "About to save document info to" << m_xmlFileName;
1239     if (!infoFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
1240         qCWarning(OkularCoreDebug) << "Failed to open docdata file" << m_xmlFileName;
1241         return;
1242     }
1243     // 1. Create DOM
1244     QDomDocument doc(QStringLiteral("documentInfo"));
1245     QDomProcessingInstruction xmlPi = doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
1246     doc.appendChild(xmlPi);
1247     QDomElement root = doc.createElement(QStringLiteral("documentInfo"));
1248     root.setAttribute(QStringLiteral("url"), m_url.toDisplayString(QUrl::PreferLocalFile));
1249     doc.appendChild(root);
1250 
1251     // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM
1252     //  -> do this if there are not-yet-migrated annots or forms in docdata/
1253     if (m_docdataMigrationNeeded) {
1254         QDomElement pageList = doc.createElement(QStringLiteral("pageList"));
1255         root.appendChild(pageList);
1256         // OriginalAnnotationPageItems and OriginalFormFieldPageItems tell to
1257         // store the same unmodified annotation list and form contents that we
1258         // read when we opened the file and ignore any change made by the user.
1259         // Since we don't store annotations and forms in docdata/ any more, this is
1260         // necessary to preserve annotations/forms that previous Okular version
1261         // had stored there.
1262         const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems | OriginalFormFieldPageItems;
1263         // <page list><page number='x'>.... </page> save pages that hold data
1264         QVector<Page *>::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd();
1265         for (; pIt != pEnd; ++pIt) {
1266             (*pIt)->d->saveLocalContents(pageList, doc, saveWhat);
1267         }
1268     }
1269 
1270     // 2.2. Save document info (current viewport, history, ... ) to DOM
1271     QDomElement generalInfo = doc.createElement(QStringLiteral("generalInfo"));
1272     root.appendChild(generalInfo);
1273     // create rotation node
1274     if (m_rotation != Rotation0) {
1275         QDomElement rotationNode = doc.createElement(QStringLiteral("rotation"));
1276         generalInfo.appendChild(rotationNode);
1277         rotationNode.appendChild(doc.createTextNode(QString::number((int)m_rotation)));
1278     }
1279     // <general info><history> ... </history> save history up to OKULAR_HISTORY_SAVEDSTEPS viewports
1280     const auto currentViewportIterator = std::list<DocumentViewport>::const_iterator(m_viewportIterator);
1281     std::list<DocumentViewport>::const_iterator backIterator = currentViewportIterator;
1282     if (backIterator != m_viewportHistory.end()) {
1283         // go back up to OKULAR_HISTORY_SAVEDSTEPS steps from the current viewportIterator
1284         int backSteps = OKULAR_HISTORY_SAVEDSTEPS;
1285         while (backSteps-- && backIterator != m_viewportHistory.begin()) {
1286             --backIterator;
1287         }
1288 
1289         // create history root node
1290         QDomElement historyNode = doc.createElement(QStringLiteral("history"));
1291         generalInfo.appendChild(historyNode);
1292 
1293         // add old[backIterator] and present[viewportIterator] items
1294         std::list<DocumentViewport>::const_iterator endIt = currentViewportIterator;
1295         ++endIt;
1296         while (backIterator != endIt) {
1297             QString name = (backIterator == currentViewportIterator) ? QStringLiteral("current") : QStringLiteral("oldPage");
1298             QDomElement historyEntry = doc.createElement(name);
1299             historyEntry.setAttribute(QStringLiteral("viewport"), (*backIterator).toString());
1300             historyNode.appendChild(historyEntry);
1301             ++backIterator;
1302         }
1303     }
1304     // create views root node
1305     QDomElement viewsNode = doc.createElement(QStringLiteral("views"));
1306     generalInfo.appendChild(viewsNode);
1307     for (View *view : std::as_const(m_views)) {
1308         QDomElement viewEntry = doc.createElement(QStringLiteral("view"));
1309         viewEntry.setAttribute(QStringLiteral("name"), view->name());
1310         viewsNode.appendChild(viewEntry);
1311         saveViewsInfo(view, viewEntry);
1312     }
1313 
1314     // 3. Save DOM to XML file
1315     QString xml = doc.toString();
1316     QTextStream os(&infoFile);
1317     os.setEncoding(QStringConverter::Utf8);
1318     os << xml;
1319     infoFile.close();
1320 }
1321 
1322 void DocumentPrivate::slotTimedMemoryCheck()
1323 {
1324     // [MEM] clean memory (for 'free mem dependent' profiles only)
1325     if (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Low && m_allocatedPixmapsTotalMemory > 1024 * 1024) {
1326         cleanupPixmapMemory();
1327     }
1328 }
1329 
1330 void DocumentPrivate::sendGeneratorPixmapRequest()
1331 {
1332     /* If the pixmap cache will have to be cleaned in order to make room for the
1333      * next request, get the distance from the current viewport of the page
1334      * whose pixmap will be removed. We will ignore preload requests for pages
1335      * that are at the same distance or farther */
1336     const qulonglong memoryToFree = calculateMemoryToFree();
1337     const int currentViewportPage = (*m_viewportIterator).pageNumber;
1338     int maxDistance = INT_MAX; // Default: No maximum
1339     if (memoryToFree) {
1340         AllocatedPixmap *pixmapToReplace = searchLowestPriorityPixmap(true);
1341         if (pixmapToReplace) {
1342             maxDistance = qAbs(pixmapToReplace->page - currentViewportPage);
1343         }
1344     }
1345 
1346     // find a request
1347     PixmapRequest *request = nullptr;
1348     m_pixmapRequestsMutex.lock();
1349     while (!m_pixmapRequestsStack.empty() && !request) {
1350         PixmapRequest *r = m_pixmapRequestsStack.back();
1351         if (!r) {
1352             m_pixmapRequestsStack.pop_back();
1353             continue;
1354         }
1355 
1356         QRect requestRect = r->isTile() ? r->normalizedRect().geometry(r->width(), r->height()) : QRect(0, 0, r->width(), r->height());
1357         TilesManager *tilesManager = r->d->tilesManager();
1358         const double normalizedArea = r->normalizedRect().width() * r->normalizedRect().height();
1359         const QScreen *screen = nullptr;
1360         if (m_widget) {
1361             const QWindow *window = m_widget->window()->windowHandle();
1362             if (window) {
1363                 screen = window->screen();
1364             }
1365         }
1366         if (!screen) {
1367             screen = QGuiApplication::primaryScreen();
1368         }
1369         const long screenSize = screen->devicePixelRatio() * screen->size().width() * screen->devicePixelRatio() * screen->size().height();
1370 
1371         // Make sure the page is the right size to receive the pixmap
1372         r->page()->setPageSize(r->observer(), r->width(), r->height());
1373 
1374         // If it's a preload but the generator is not threaded no point in trying to preload
1375         if (r->preload() && !m_generator->hasFeature(Generator::Threaded)) {
1376             m_pixmapRequestsStack.pop_back();
1377             delete r;
1378         }
1379         // request only if page isn't already present and request has valid id
1380         else if ((!r->d->mForce && r->page()->hasPixmap(r->observer(), r->width(), r->height(), r->normalizedRect())) || !m_observers.contains(r->observer())) {
1381             m_pixmapRequestsStack.pop_back();
1382             delete r;
1383         } else if (!r->d->mForce && r->preload() && qAbs(r->pageNumber() - currentViewportPage) >= maxDistance) {
1384             m_pixmapRequestsStack.pop_back();
1385             // qCDebug(OkularCoreDebug) << "Ignoring request that doesn't fit in cache";
1386             delete r;
1387         }
1388         // Ignore requests for pixmaps that are already being generated
1389         else if (tilesManager && tilesManager->isRequesting(r->normalizedRect(), r->width(), r->height())) {
1390             m_pixmapRequestsStack.pop_back();
1391             delete r;
1392         }
1393         // If the requested area is above 4*screenSize pixels, and we're not rendering most of the page,  switch on the tile manager
1394         else if (!tilesManager && m_generator->hasFeature(Generator::TiledRendering) && (long)r->width() * (long)r->height() > 4L * screenSize && normalizedArea < 0.75) {
1395             // if the image is too big. start using tiles
1396             qCDebug(OkularCoreDebug).nospace() << "Start using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1397 
1398             // fill the tiles manager with the last rendered pixmap
1399             const QPixmap *pixmap = r->page()->_o_nearestPixmap(r->observer(), r->width(), r->height());
1400             if (pixmap) {
1401                 tilesManager = new TilesManager(r->pageNumber(), pixmap->width(), pixmap->height(), r->page()->rotation());
1402                 tilesManager->setPixmap(pixmap, NormalizedRect(0, 0, 1, 1), true /*isPartialPixmap*/);
1403                 tilesManager->setSize(r->width(), r->height());
1404             } else {
1405                 // create new tiles manager
1406                 tilesManager = new TilesManager(r->pageNumber(), r->width(), r->height(), r->page()->rotation());
1407             }
1408             tilesManager->setRequest(r->normalizedRect(), r->width(), r->height());
1409             r->page()->deletePixmap(r->observer());
1410             r->page()->d->setTilesManager(r->observer(), tilesManager);
1411             r->setTile(true);
1412 
1413             // Change normalizedRect to the smallest rect that contains all
1414             // visible tiles.
1415             if (!r->normalizedRect().isNull()) {
1416                 NormalizedRect tilesRect;
1417                 const QList<Tile> tiles = tilesManager->tilesAt(r->normalizedRect(), TilesManager::TerminalTile);
1418                 QList<Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
1419                 while (tIt != tEnd) {
1420                     Tile tile = *tIt;
1421                     if (tilesRect.isNull()) {
1422                         tilesRect = tile.rect();
1423                     } else {
1424                         tilesRect |= tile.rect();
1425                     }
1426 
1427                     ++tIt;
1428                 }
1429 
1430                 r->setNormalizedRect(tilesRect);
1431                 request = r;
1432             } else {
1433                 // Discard request if normalizedRect is null. This happens in
1434                 // preload requests issued by PageView if the requested page is
1435                 // not visible and the user has just switched from a non-tiled
1436                 // zoom level to a tiled one
1437                 m_pixmapRequestsStack.pop_back();
1438                 delete r;
1439             }
1440         }
1441         // If the requested area is below 3*screenSize pixels, switch off the tile manager
1442         else if (tilesManager && (long)r->width() * (long)r->height() < 3L * screenSize) {
1443             qCDebug(OkularCoreDebug).nospace() << "Stop using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1444 
1445             // page is too small. stop using tiles.
1446             r->page()->deletePixmap(r->observer());
1447             r->setTile(false);
1448 
1449             request = r;
1450         } else if ((long)requestRect.width() * (long)requestRect.height() > 100L * screenSize && (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Greedy)) {
1451             m_pixmapRequestsStack.pop_back();
1452             if (!m_warnedOutOfMemory) {
1453                 qCWarning(OkularCoreDebug).nospace() << "Running out of memory on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1454                 qCWarning(OkularCoreDebug) << "this message will be reported only once.";
1455                 m_warnedOutOfMemory = true;
1456             }
1457             delete r;
1458         } else {
1459             request = r;
1460         }
1461     }
1462 
1463     // if no request found (or already generated), return
1464     if (!request) {
1465         m_pixmapRequestsMutex.unlock();
1466         return;
1467     }
1468 
1469     // [MEM] preventive memory freeing
1470     qulonglong pixmapBytes = 0;
1471     TilesManager *tm = request->d->tilesManager();
1472     if (tm) {
1473         pixmapBytes = tm->totalMemory();
1474     } else {
1475         pixmapBytes = 4 * request->width() * request->height();
1476     }
1477 
1478     if (pixmapBytes > (1024 * 1024)) {
1479         cleanupPixmapMemory(memoryToFree /* previously calculated value */);
1480     }
1481 
1482     // submit the request to the generator
1483     if (m_generator->canGeneratePixmap()) {
1484         QRect requestRect = !request->isTile() ? QRect(0, 0, request->width(), request->height()) : request->normalizedRect().geometry(request->width(), request->height());
1485         qCDebug(OkularCoreDebug).nospace() << "sending request observer=" << request->observer() << " " << requestRect.width() << "x" << requestRect.height() << "@" << request->pageNumber() << " async == " << request->asynchronous()
1486                                            << " isTile == " << request->isTile();
1487         m_pixmapRequestsStack.remove(request);
1488 
1489         if (tm) {
1490             tm->setRequest(request->normalizedRect(), request->width(), request->height());
1491         }
1492 
1493         if ((int)m_rotation % 2) {
1494             request->d->swap();
1495         }
1496 
1497         if (m_rotation != Rotation0 && !request->normalizedRect().isNull()) {
1498             request->setNormalizedRect(TilesManager::fromRotatedRect(request->normalizedRect(), m_rotation));
1499         }
1500 
1501         // If set elsewhere we already know we want it to be partial
1502         if (!request->partialUpdatesWanted()) {
1503             request->setPartialUpdatesWanted(request->asynchronous() && !request->page()->hasPixmap(request->observer()));
1504         }
1505 
1506         // we always have to unlock _before_ the generatePixmap() because
1507         // a sync generation would end with requestDone() -> deadlock, and
1508         // we can not really know if the generator can do async requests
1509         m_executingPixmapRequests.push_back(request);
1510         m_pixmapRequestsMutex.unlock();
1511         m_generator->generatePixmap(request);
1512     } else {
1513         m_pixmapRequestsMutex.unlock();
1514         // pino (7/4/2006): set the polling interval from 10 to 30
1515         QTimer::singleShot(30, m_parent, [this] { sendGeneratorPixmapRequest(); });
1516     }
1517 }
1518 
1519 void DocumentPrivate::rotationFinished(int page, Okular::Page *okularPage)
1520 {
1521     Okular::Page *wantedPage = m_pagesVector.value(page, nullptr);
1522     if (!wantedPage || wantedPage != okularPage) {
1523         return;
1524     }
1525 
1526     for (DocumentObserver *o : std::as_const(m_observers)) {
1527         o->notifyPageChanged(page, DocumentObserver::Pixmap | DocumentObserver::Annotations);
1528     }
1529 }
1530 
1531 void DocumentPrivate::slotFontReadingProgress(int page)
1532 {
1533     Q_EMIT m_parent->fontReadingProgress(page);
1534 
1535     if (page >= (int)m_parent->pages() - 1) {
1536         Q_EMIT m_parent->fontReadingEnded();
1537         m_fontThread = nullptr;
1538         m_fontsCached = true;
1539     }
1540 }
1541 
1542 void DocumentPrivate::fontReadingGotFont(const Okular::FontInfo &font)
1543 {
1544     // Try to avoid duplicate fonts
1545     if (m_fontsCache.indexOf(font) == -1) {
1546         m_fontsCache.append(font);
1547 
1548         Q_EMIT m_parent->gotFont(font);
1549     }
1550 }
1551 
1552 void DocumentPrivate::slotGeneratorConfigChanged()
1553 {
1554     if (!m_generator) {
1555         return;
1556     }
1557 
1558     // reparse generator config and if something changed clear Pages
1559     bool configchanged = false;
1560     QHash<QString, GeneratorInfo>::iterator it = m_loadedGenerators.begin(), itEnd = m_loadedGenerators.end();
1561     for (; it != itEnd; ++it) {
1562         Okular::ConfigInterface *iface = generatorConfig(it.value());
1563         if (iface) {
1564             bool it_changed = iface->reparseConfig();
1565             if (it_changed && (m_generator == it.value().generator)) {
1566                 configchanged = true;
1567             }
1568         }
1569     }
1570     if (configchanged) {
1571         // invalidate pixmaps
1572         QVector<Page *>::const_iterator it = m_pagesVector.constBegin(), end = m_pagesVector.constEnd();
1573         for (; it != end; ++it) {
1574             (*it)->deletePixmaps();
1575         }
1576 
1577         // [MEM] remove allocation descriptors
1578         qDeleteAll(m_allocatedPixmaps);
1579         m_allocatedPixmaps.clear();
1580         m_allocatedPixmapsTotalMemory = 0;
1581 
1582         // send reload signals to observers
1583         foreachObserverD(notifyContentsCleared(DocumentObserver::Pixmap));
1584     }
1585 
1586     // free memory if in 'low' profile
1587     if (SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !m_allocatedPixmaps.empty() && !m_pagesVector.isEmpty()) {
1588         cleanupPixmapMemory();
1589     }
1590 }
1591 
1592 void DocumentPrivate::refreshPixmaps(int pageNumber)
1593 {
1594     Page *page = m_pagesVector.value(pageNumber, nullptr);
1595     if (!page) {
1596         return;
1597     }
1598 
1599     QMap<DocumentObserver *, PagePrivate::PixmapObject>::ConstIterator it = page->d->m_pixmaps.constBegin(), itEnd = page->d->m_pixmaps.constEnd();
1600     QVector<Okular::PixmapRequest *> pixmapsToRequest;
1601     for (; it != itEnd; ++it) {
1602         const QSize size = (*it).m_pixmap->size();
1603         PixmapRequest *p = new PixmapRequest(it.key(), pageNumber, size.width(), size.height(), 1 /* dpr */, 1, PixmapRequest::Asynchronous);
1604         p->d->mForce = true;
1605         pixmapsToRequest << p;
1606     }
1607 
1608     // Need to do this ↑↓ in two steps since requestPixmaps can end up calling cancelRenderingBecauseOf
1609     // which changes m_pixmaps and thus breaks the loop above
1610     for (PixmapRequest *pr : std::as_const(pixmapsToRequest)) {
1611         QList<Okular::PixmapRequest *> requestedPixmaps;
1612         requestedPixmaps.push_back(pr);
1613         m_parent->requestPixmaps(requestedPixmaps, Okular::Document::NoOption);
1614     }
1615 
1616     for (DocumentObserver *observer : std::as_const(m_observers)) {
1617         QList<Okular::PixmapRequest *> requestedPixmaps;
1618 
1619         TilesManager *tilesManager = page->d->tilesManager(observer);
1620         if (tilesManager) {
1621             tilesManager->markDirty();
1622 
1623             PixmapRequest *p = new PixmapRequest(observer, pageNumber, tilesManager->width(), tilesManager->height(), 1 /* dpr */, 1, PixmapRequest::Asynchronous);
1624 
1625             // Get the visible page rect
1626             NormalizedRect visibleRect;
1627             QVector<Okular::VisiblePageRect *>::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd();
1628             for (; vIt != vEnd; ++vIt) {
1629                 if ((*vIt)->pageNumber == pageNumber) {
1630                     visibleRect = (*vIt)->rect;
1631                     break;
1632                 }
1633             }
1634 
1635             if (!visibleRect.isNull()) {
1636                 p->setNormalizedRect(visibleRect);
1637                 p->setTile(true);
1638                 p->d->mForce = true;
1639                 requestedPixmaps.push_back(p);
1640             } else {
1641                 delete p;
1642             }
1643         }
1644 
1645         m_parent->requestPixmaps(requestedPixmaps, Okular::Document::NoOption);
1646     }
1647 }
1648 
1649 void DocumentPrivate::_o_configChanged()
1650 {
1651     // free text pages if needed
1652     calculateMaxTextPages();
1653     while (m_allocatedTextPagesFifo.count() > m_maxAllocatedTextPages) {
1654         int pageToKick = m_allocatedTextPagesFifo.takeFirst();
1655         m_pagesVector.at(pageToKick)->setTextPage(nullptr); // deletes the textpage
1656     }
1657 }
1658 
1659 void DocumentPrivate::doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct)
1660 {
1661     DoContinueDirectionMatchSearchStruct *searchStruct = static_cast<DoContinueDirectionMatchSearchStruct *>(doContinueDirectionMatchSearchStruct);
1662     RunningSearch *search = m_searches.value(searchStruct->searchID);
1663 
1664     if ((m_searchCancelled && !searchStruct->match) || !search) {
1665         // if the user cancelled but he just got a match, give him the match!
1666         QApplication::restoreOverrideCursor();
1667 
1668         if (search) {
1669             search->isCurrentlySearching = false;
1670         }
1671 
1672         Q_EMIT m_parent->searchFinished(searchStruct->searchID, Document::SearchCancelled);
1673         delete searchStruct->pagesToNotify;
1674         delete searchStruct;
1675         return;
1676     }
1677 
1678     const bool forward = search->cachedType == Document::NextMatch;
1679     bool doContinue = false;
1680     // if no match found, loop through the whole doc, starting from currentPage
1681     if (!searchStruct->match) {
1682         const int pageCount = m_pagesVector.count();
1683         if (search->pagesDone < pageCount) {
1684             doContinue = true;
1685             if (searchStruct->currentPage >= pageCount) {
1686                 searchStruct->currentPage = 0;
1687                 Q_EMIT m_parent->notice(i18n("Continuing search from beginning"), 3000);
1688             } else if (searchStruct->currentPage < 0) {
1689                 searchStruct->currentPage = pageCount - 1;
1690                 Q_EMIT m_parent->notice(i18n("Continuing search from bottom"), 3000);
1691             }
1692         }
1693     }
1694 
1695     if (doContinue) {
1696         // get page
1697         Page *page = m_pagesVector[searchStruct->currentPage];
1698         // request search page if needed
1699         if (!page->hasTextPage()) {
1700             m_parent->requestTextPage(page->number());
1701         }
1702 
1703         // if found a match on the current page, end the loop
1704         searchStruct->match = page->findText(searchStruct->searchID, search->cachedString, forward ? FromTop : FromBottom, search->cachedCaseSensitivity);
1705         if (!searchStruct->match) {
1706             if (forward) {
1707                 searchStruct->currentPage++;
1708             } else {
1709                 searchStruct->currentPage--;
1710             }
1711             search->pagesDone++;
1712         } else {
1713             search->pagesDone = 1;
1714         }
1715 
1716         // Both of the previous if branches need to call doContinueDirectionMatchSearch
1717         QTimer::singleShot(0, m_parent, [this, searchStruct] { doContinueDirectionMatchSearch(searchStruct); });
1718     } else {
1719         doProcessSearchMatch(searchStruct->match, search, searchStruct->pagesToNotify, searchStruct->currentPage, searchStruct->searchID, search->cachedViewportMove, search->cachedColor);
1720         delete searchStruct;
1721     }
1722 }
1723 
1724 void DocumentPrivate::doProcessSearchMatch(RegularAreaRect *match, RunningSearch *search, QSet<int> *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor &color)
1725 {
1726     // reset cursor to previous shape
1727     QApplication::restoreOverrideCursor();
1728 
1729     bool foundAMatch = false;
1730 
1731     search->isCurrentlySearching = false;
1732 
1733     // if a match has been found..
1734     if (match) {
1735         // update the RunningSearch structure adding this match..
1736         foundAMatch = true;
1737         search->continueOnPage = currentPage;
1738         search->continueOnMatch = *match;
1739         search->highlightedPages.insert(currentPage);
1740         // ..add highlight to the page..
1741         m_pagesVector[currentPage]->d->setHighlight(searchID, match, color);
1742 
1743         // ..queue page for notifying changes..
1744         pagesToNotify->insert(currentPage);
1745 
1746         // Create a normalized rectangle around the search match that includes a 5% buffer on all sides.
1747         const Okular::NormalizedRect matchRectWithBuffer = Okular::NormalizedRect(match->first().left - 0.05, match->first().top - 0.05, match->first().right + 0.05, match->first().bottom + 0.05);
1748 
1749         const bool matchRectFullyVisible = isNormalizedRectangleFullyVisible(matchRectWithBuffer, currentPage);
1750 
1751         // ..move the viewport to show the first of the searched word sequence centered
1752         if (moveViewport && !matchRectFullyVisible) {
1753             DocumentViewport searchViewport(currentPage);
1754             searchViewport.rePos.enabled = true;
1755             searchViewport.rePos.normalizedX = (match->first().left + match->first().right) / 2.0;
1756             searchViewport.rePos.normalizedY = (match->first().top + match->first().bottom) / 2.0;
1757             m_parent->setViewport(searchViewport, nullptr, true);
1758         }
1759         delete match;
1760     }
1761 
1762     // notify observers about highlights changes
1763     for (int pageNumber : std::as_const(*pagesToNotify)) {
1764         for (DocumentObserver *observer : std::as_const(m_observers)) {
1765             observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1766         }
1767     }
1768 
1769     if (foundAMatch) {
1770         Q_EMIT m_parent->searchFinished(searchID, Document::MatchFound);
1771     } else {
1772         Q_EMIT m_parent->searchFinished(searchID, Document::NoMatchFound);
1773     }
1774 
1775     delete pagesToNotify;
1776 }
1777 
1778 void DocumentPrivate::doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID)
1779 {
1780     QMap<Page *, QVector<RegularAreaRect *>> *pageMatches = static_cast<QMap<Page *, QVector<RegularAreaRect *>> *>(pageMatchesMap);
1781     QSet<int> *pagesToNotify = static_cast<QSet<int> *>(pagesToNotifySet);
1782     RunningSearch *search = m_searches.value(searchID);
1783 
1784     if (m_searchCancelled || !search) {
1785         typedef QVector<RegularAreaRect *> MatchesVector;
1786 
1787         QApplication::restoreOverrideCursor();
1788 
1789         if (search) {
1790             search->isCurrentlySearching = false;
1791         }
1792 
1793         Q_EMIT m_parent->searchFinished(searchID, Document::SearchCancelled);
1794         for (const MatchesVector &mv : std::as_const(*pageMatches)) {
1795             qDeleteAll(mv);
1796         }
1797         delete pageMatches;
1798         delete pagesToNotify;
1799         return;
1800     }
1801 
1802     if (currentPage < m_pagesVector.count()) {
1803         // get page (from the first to the last)
1804         Page *page = m_pagesVector.at(currentPage);
1805         int pageNumber = page->number(); // redundant? is it == currentPage ?
1806 
1807         // request search page if needed
1808         if (!page->hasTextPage()) {
1809             m_parent->requestTextPage(pageNumber);
1810         }
1811 
1812         // loop on a page adding highlights for all found items
1813         RegularAreaRect *lastMatch = nullptr;
1814         while (true) {
1815             if (lastMatch) {
1816                 lastMatch = page->findText(searchID, search->cachedString, NextResult, search->cachedCaseSensitivity, lastMatch);
1817             } else {
1818                 lastMatch = page->findText(searchID, search->cachedString, FromTop, search->cachedCaseSensitivity);
1819             }
1820 
1821             if (!lastMatch) {
1822                 break;
1823             }
1824 
1825             // add highlight rect to the matches map
1826             (*pageMatches)[page].append(lastMatch);
1827         }
1828         delete lastMatch;
1829 
1830         QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID] { doContinueAllDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID); });
1831     } else {
1832         // reset cursor to previous shape
1833         QApplication::restoreOverrideCursor();
1834 
1835         search->isCurrentlySearching = false;
1836         bool foundAMatch = pageMatches->count() != 0;
1837         QMap<Page *, QVector<RegularAreaRect *>>::const_iterator it, itEnd;
1838         it = pageMatches->constBegin();
1839         itEnd = pageMatches->constEnd();
1840         for (; it != itEnd; ++it) {
1841             for (RegularAreaRect *match : it.value()) {
1842                 it.key()->d->setHighlight(searchID, match, search->cachedColor);
1843                 delete match;
1844             }
1845             search->highlightedPages.insert(it.key()->number());
1846             pagesToNotify->insert(it.key()->number());
1847         }
1848 
1849         for (DocumentObserver *observer : std::as_const(m_observers)) {
1850             observer->notifySetup(m_pagesVector, 0);
1851         }
1852 
1853         // notify observers about highlights changes
1854         for (int pageNumber : std::as_const(*pagesToNotify)) {
1855             for (DocumentObserver *observer : std::as_const(m_observers)) {
1856                 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1857             }
1858         }
1859 
1860         if (foundAMatch) {
1861             Q_EMIT m_parent->searchFinished(searchID, Document::MatchFound);
1862         } else {
1863             Q_EMIT m_parent->searchFinished(searchID, Document::NoMatchFound);
1864         }
1865 
1866         delete pageMatches;
1867         delete pagesToNotify;
1868     }
1869 }
1870 
1871 void DocumentPrivate::doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList &words)
1872 {
1873     typedef QPair<RegularAreaRect *, QColor> MatchColor;
1874     QMap<Page *, QVector<MatchColor>> *pageMatches = static_cast<QMap<Page *, QVector<MatchColor>> *>(pageMatchesMap);
1875     QSet<int> *pagesToNotify = static_cast<QSet<int> *>(pagesToNotifySet);
1876     RunningSearch *search = m_searches.value(searchID);
1877 
1878     if (m_searchCancelled || !search) {
1879         typedef QVector<MatchColor> MatchesVector;
1880 
1881         QApplication::restoreOverrideCursor();
1882 
1883         if (search) {
1884             search->isCurrentlySearching = false;
1885         }
1886 
1887         Q_EMIT m_parent->searchFinished(searchID, Document::SearchCancelled);
1888 
1889         for (const MatchesVector &mv : std::as_const(*pageMatches)) {
1890             for (const MatchColor &mc : mv) {
1891                 delete mc.first;
1892             }
1893         }
1894         delete pageMatches;
1895         delete pagesToNotify;
1896         return;
1897     }
1898 
1899     const int wordCount = words.count();
1900     const int hueStep = (wordCount > 1) ? (60 / (wordCount - 1)) : 60;
1901     int baseHue, baseSat, baseVal;
1902     search->cachedColor.getHsv(&baseHue, &baseSat, &baseVal);
1903 
1904     if (currentPage < m_pagesVector.count()) {
1905         // get page (from the first to the last)
1906         Page *page = m_pagesVector.at(currentPage);
1907         int pageNumber = page->number(); // redundant? is it == currentPage ?
1908 
1909         // request search page if needed
1910         if (!page->hasTextPage()) {
1911             m_parent->requestTextPage(pageNumber);
1912         }
1913 
1914         // loop on a page adding highlights for all found items
1915         bool allMatched = wordCount > 0, anyMatched = false;
1916         for (int w = 0; w < wordCount; w++) {
1917             const QString &word = words[w];
1918             int newHue = baseHue - w * hueStep;
1919             if (newHue < 0) {
1920                 newHue += 360;
1921             }
1922             QColor wordColor = QColor::fromHsv(newHue, baseSat, baseVal);
1923             RegularAreaRect *lastMatch = nullptr;
1924             // add all highlights for current word
1925             bool wordMatched = false;
1926             while (true) {
1927                 if (lastMatch) {
1928                     lastMatch = page->findText(searchID, word, NextResult, search->cachedCaseSensitivity, lastMatch);
1929                 } else {
1930                     lastMatch = page->findText(searchID, word, FromTop, search->cachedCaseSensitivity);
1931                 }
1932 
1933                 if (!lastMatch) {
1934                     break;
1935                 }
1936 
1937                 // add highligh rect to the matches map
1938                 (*pageMatches)[page].append(MatchColor(lastMatch, wordColor));
1939                 wordMatched = true;
1940             }
1941             allMatched = allMatched && wordMatched;
1942             anyMatched = anyMatched || wordMatched;
1943         }
1944 
1945         // if not all words are present in page, remove partial highlights
1946         const bool matchAll = search->cachedType == Document::GoogleAll;
1947         if (!allMatched && matchAll) {
1948             const QVector<MatchColor> &matches = (*pageMatches)[page];
1949             for (const MatchColor &mc : matches) {
1950                 delete mc.first;
1951             }
1952             pageMatches->remove(page);
1953         }
1954 
1955         QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID, words] { doContinueGooglesDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID, words); });
1956     } else {
1957         // reset cursor to previous shape
1958         QApplication::restoreOverrideCursor();
1959 
1960         search->isCurrentlySearching = false;
1961         bool foundAMatch = pageMatches->count() != 0;
1962         QMap<Page *, QVector<MatchColor>>::const_iterator it, itEnd;
1963         it = pageMatches->constBegin();
1964         itEnd = pageMatches->constEnd();
1965         for (; it != itEnd; ++it) {
1966             for (const MatchColor &mc : it.value()) {
1967                 it.key()->d->setHighlight(searchID, mc.first, mc.second);
1968                 delete mc.first;
1969             }
1970             search->highlightedPages.insert(it.key()->number());
1971             pagesToNotify->insert(it.key()->number());
1972         }
1973 
1974         // send page lists to update observers (since some filter on bookmarks)
1975         for (DocumentObserver *observer : std::as_const(m_observers)) {
1976             observer->notifySetup(m_pagesVector, 0);
1977         }
1978 
1979         // notify observers about highlights changes
1980         for (int pageNumber : std::as_const(*pagesToNotify)) {
1981             for (DocumentObserver *observer : std::as_const(m_observers)) {
1982                 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1983             }
1984         }
1985 
1986         if (foundAMatch) {
1987             Q_EMIT m_parent->searchFinished(searchID, Document::MatchFound);
1988         } else {
1989             Q_EMIT m_parent->searchFinished(searchID, Document::NoMatchFound);
1990         }
1991 
1992         delete pageMatches;
1993         delete pagesToNotify;
1994     }
1995 }
1996 
1997 QVariant DocumentPrivate::documentMetaData(const Generator::DocumentMetaDataKey key, const QVariant &option) const
1998 {
1999     switch (key) {
2000     case Generator::PaperColorMetaData: {
2001         bool giveDefault = option.toBool();
2002         QColor color;
2003         if ((SettingsCore::renderMode() == SettingsCore::EnumRenderMode::Paper) && SettingsCore::changeColors()) {
2004             color = SettingsCore::paperColor();
2005         } else if (giveDefault) {
2006             color = Qt::white;
2007         }
2008         return color;
2009     } break;
2010 
2011     case Generator::TextAntialiasMetaData:
2012         switch (SettingsCore::textAntialias()) {
2013         case SettingsCore::EnumTextAntialias::Enabled:
2014             return true;
2015             break;
2016         case SettingsCore::EnumTextAntialias::Disabled:
2017             return false;
2018             break;
2019         }
2020         break;
2021 
2022     case Generator::GraphicsAntialiasMetaData:
2023         switch (SettingsCore::graphicsAntialias()) {
2024         case SettingsCore::EnumGraphicsAntialias::Enabled:
2025             return true;
2026             break;
2027         case SettingsCore::EnumGraphicsAntialias::Disabled:
2028             return false;
2029             break;
2030         }
2031         break;
2032 
2033     case Generator::TextHintingMetaData:
2034         switch (SettingsCore::textHinting()) {
2035         case SettingsCore::EnumTextHinting::Enabled:
2036             return true;
2037             break;
2038         case SettingsCore::EnumTextHinting::Disabled:
2039             return false;
2040             break;
2041         }
2042         break;
2043     }
2044     return QVariant();
2045 }
2046 
2047 bool DocumentPrivate::isNormalizedRectangleFullyVisible(const Okular::NormalizedRect &rectOfInterest, int rectPage)
2048 {
2049     bool rectFullyVisible = false;
2050     const QVector<Okular::VisiblePageRect *> &visibleRects = m_parent->visiblePageRects();
2051     QVector<Okular::VisiblePageRect *>::const_iterator vEnd = visibleRects.end();
2052     QVector<Okular::VisiblePageRect *>::const_iterator vIt = visibleRects.begin();
2053 
2054     for (; (vIt != vEnd) && !rectFullyVisible; ++vIt) {
2055         if ((*vIt)->pageNumber == rectPage && (*vIt)->rect.contains(rectOfInterest.left, rectOfInterest.top) && (*vIt)->rect.contains(rectOfInterest.right, rectOfInterest.bottom)) {
2056             rectFullyVisible = true;
2057         }
2058     }
2059     return rectFullyVisible;
2060 }
2061 
2062 struct pdfsyncpoint {
2063     QString file;
2064     qlonglong x;
2065     qlonglong y;
2066     int row;
2067     int column;
2068     int page;
2069 };
2070 
2071 void DocumentPrivate::loadSyncFile(const QString &filePath)
2072 {
2073     QFile f(filePath + QLatin1String("sync"));
2074     if (!f.open(QIODevice::ReadOnly)) {
2075         return;
2076     }
2077 
2078     QTextStream ts(&f);
2079     // first row: core name of the pdf output
2080     const QString coreName = ts.readLine();
2081     // second row: version string, in the form 'Version %u'
2082     const QString versionstr = ts.readLine();
2083     // anchor the pattern with \A and \z to match the entire subject string
2084     // TODO: with Qt 5.12 QRegularExpression::anchoredPattern() can be used instead
2085     static QRegularExpression versionre(QStringLiteral("\\AVersion \\d+\\z"), QRegularExpression::CaseInsensitiveOption);
2086     QRegularExpressionMatch match = versionre.match(versionstr);
2087     if (!match.hasMatch()) {
2088         return;
2089     }
2090 
2091     QHash<int, pdfsyncpoint> points;
2092     QStack<QString> fileStack;
2093     int currentpage = -1;
2094     const QLatin1String texStr(".tex");
2095     const QChar spaceChar = QChar::fromLatin1(' ');
2096 
2097     fileStack.push(coreName + texStr);
2098 
2099     const QSizeF dpi = m_generator->dpi();
2100 
2101     QString line;
2102     while (!ts.atEnd()) {
2103         line = ts.readLine();
2104         const QStringList tokens = line.split(spaceChar, Qt::SkipEmptyParts);
2105         const int tokenSize = tokens.count();
2106         if (tokenSize < 1) {
2107             continue;
2108         }
2109         if (tokens.first() == QLatin1String("l") && tokenSize >= 3) {
2110             int id = tokens.at(1).toInt();
2111             QHash<int, pdfsyncpoint>::const_iterator it = points.constFind(id);
2112             if (it == points.constEnd()) {
2113                 pdfsyncpoint pt;
2114                 pt.x = 0;
2115                 pt.y = 0;
2116                 pt.row = tokens.at(2).toInt();
2117                 pt.column = 0; // TODO
2118                 pt.page = -1;
2119                 pt.file = fileStack.top();
2120                 points[id] = pt;
2121             }
2122         } else if (tokens.first() == QLatin1String("s") && tokenSize >= 2) {
2123             currentpage = tokens.at(1).toInt() - 1;
2124         } else if (tokens.first() == QLatin1String("p*") && tokenSize >= 4) {
2125             // TODO
2126             qCDebug(OkularCoreDebug) << "PdfSync: 'p*' line ignored";
2127         } else if (tokens.first() == QLatin1String("p") && tokenSize >= 4) {
2128             int id = tokens.at(1).toInt();
2129             QHash<int, pdfsyncpoint>::iterator it = points.find(id);
2130             if (it != points.end()) {
2131                 it->x = tokens.at(2).toInt();
2132                 it->y = tokens.at(3).toInt();
2133                 it->page = currentpage;
2134             }
2135         } else if (line.startsWith(QLatin1Char('(')) && tokenSize == 1) {
2136             QString newfile = line;
2137             // chop the leading '('
2138             newfile.remove(0, 1);
2139             if (!newfile.endsWith(texStr)) {
2140                 newfile += texStr;
2141             }
2142             fileStack.push(newfile);
2143         } else if (line == QLatin1String(")")) {
2144             if (!fileStack.isEmpty()) {
2145                 fileStack.pop();
2146             } else {
2147                 qCDebug(OkularCoreDebug) << "PdfSync: going one level down too much";
2148             }
2149         } else {
2150             qCDebug(OkularCoreDebug).nospace() << "PdfSync: unknown line format: '" << line << "'";
2151         }
2152     }
2153 
2154     QVector<QList<Okular::SourceRefObjectRect *>> refRects(m_pagesVector.size());
2155     for (const pdfsyncpoint &pt : std::as_const(points)) {
2156         // drop pdfsync points not completely valid
2157         if (pt.page < 0 || pt.page >= m_pagesVector.size()) {
2158             continue;
2159         }
2160 
2161         // magic numbers for TeX's RSU's (Ridiculously Small Units) conversion to pixels
2162         Okular::NormalizedPoint p((pt.x * dpi.width()) / (72.27 * 65536.0 * m_pagesVector[pt.page]->width()), (pt.y * dpi.height()) / (72.27 * 65536.0 * m_pagesVector[pt.page]->height()));
2163         QString file = pt.file;
2164         Okular::SourceReference *sourceRef = new Okular::SourceReference(file, pt.row, pt.column);
2165         refRects[pt.page].append(new Okular::SourceRefObjectRect(p, sourceRef));
2166     }
2167     for (int i = 0; i < refRects.size(); ++i) {
2168         if (!refRects.at(i).isEmpty()) {
2169             m_pagesVector[i]->setSourceReferences(refRects.at(i));
2170         }
2171     }
2172 }
2173 
2174 void DocumentPrivate::clearAndWaitForRequests()
2175 {
2176     m_pixmapRequestsMutex.lock();
2177     std::list<PixmapRequest *>::const_iterator sIt = m_pixmapRequestsStack.begin();
2178     std::list<PixmapRequest *>::const_iterator sEnd = m_pixmapRequestsStack.end();
2179     for (; sIt != sEnd; ++sIt) {
2180         delete *sIt;
2181     }
2182     m_pixmapRequestsStack.clear();
2183     m_pixmapRequestsMutex.unlock();
2184 
2185     QEventLoop loop;
2186     bool startEventLoop = false;
2187     do {
2188         m_pixmapRequestsMutex.lock();
2189         startEventLoop = !m_executingPixmapRequests.empty();
2190 
2191         if (m_generator->hasFeature(Generator::SupportsCancelling)) {
2192             for (PixmapRequest *executingRequest : std::as_const(m_executingPixmapRequests)) {
2193                 executingRequest->d->mShouldAbortRender = 1;
2194             }
2195 
2196             if (m_generator->d_ptr->mTextPageGenerationThread) {
2197                 m_generator->d_ptr->mTextPageGenerationThread->abortExtraction();
2198             }
2199         }
2200 
2201         m_pixmapRequestsMutex.unlock();
2202         if (startEventLoop) {
2203             m_closingLoop = &loop;
2204             loop.exec();
2205             m_closingLoop = nullptr;
2206         }
2207     } while (startEventLoop);
2208 }
2209 
2210 int DocumentPrivate::findFieldPageNumber(Okular::FormField *field)
2211 {
2212     // Lookup the page of the FormField
2213     int foundPage = -1;
2214     for (uint pageIdx = 0, nPages = m_parent->pages(); pageIdx < nPages; pageIdx++) {
2215         const Page *p = m_parent->page(pageIdx);
2216         if (p && p->formFields().contains(field)) {
2217             foundPage = static_cast<int>(pageIdx);
2218             break;
2219         }
2220     }
2221     return foundPage;
2222 }
2223 
2224 void DocumentPrivate::executeScriptEvent(const std::shared_ptr<Event> &event, const Okular::ScriptAction *linkscript)
2225 {
2226     if (!m_scripter) {
2227         m_scripter = new Scripter(this);
2228     }
2229     m_scripter->setEvent(event.get());
2230     m_scripter->execute(linkscript->scriptType(), linkscript->script());
2231 
2232     // Clear out the event after execution
2233     m_scripter->setEvent(nullptr);
2234 }
2235 
2236 Document::Document(QWidget *widget)
2237     : QObject(nullptr)
2238     , d(new DocumentPrivate(this))
2239 {
2240     d->m_widget = widget;
2241     d->m_bookmarkManager = new BookmarkManager(d);
2242     d->m_viewportIterator = d->m_viewportHistory.insert(d->m_viewportHistory.end(), DocumentViewport());
2243     d->m_undoStack = new QUndoStack(this);
2244 
2245     connect(SettingsCore::self(), &SettingsCore::configChanged, this, [this] { d->_o_configChanged(); });
2246     connect(d->m_undoStack, &QUndoStack::canUndoChanged, this, &Document::canUndoChanged);
2247     connect(d->m_undoStack, &QUndoStack::canRedoChanged, this, &Document::canRedoChanged);
2248     connect(d->m_undoStack, &QUndoStack::cleanChanged, this, &Document::undoHistoryCleanChanged);
2249 
2250     qRegisterMetaType<Okular::FontInfo>();
2251 }
2252 
2253 Document::~Document()
2254 {
2255     // delete generator, pages, and related stuff
2256     closeDocument();
2257 
2258     QSet<View *>::const_iterator viewIt = d->m_views.constBegin(), viewEnd = d->m_views.constEnd();
2259     for (; viewIt != viewEnd; ++viewIt) {
2260         View *v = *viewIt;
2261         v->d_func()->document = nullptr;
2262     }
2263 
2264     // delete the bookmark manager
2265     delete d->m_bookmarkManager;
2266 
2267     // delete the loaded generators
2268     QHash<QString, GeneratorInfo>::const_iterator it = d->m_loadedGenerators.constBegin(), itEnd = d->m_loadedGenerators.constEnd();
2269     for (; it != itEnd; ++it) {
2270         d->unloadGenerator(it.value());
2271     }
2272     d->m_loadedGenerators.clear();
2273 
2274     // delete the private structure
2275     delete d;
2276 }
2277 
2278 QString DocumentPrivate::docDataFileName(const QUrl &url, qint64 document_size)
2279 {
2280     QString fn = url.fileName();
2281     fn = QString::number(document_size) + QLatin1Char('.') + fn + QStringLiteral(".xml");
2282     QString docdataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/okular/docdata");
2283     // make sure that the okular/docdata/ directory exists (probably this used to be handled by KStandardDirs)
2284     if (!QFileInfo::exists(docdataDir)) {
2285         qCDebug(OkularCoreDebug) << "creating docdata folder" << docdataDir;
2286         QDir().mkpath(docdataDir);
2287     }
2288     QString newokularfile = docdataDir + QLatin1Char('/') + fn;
2289 
2290     return newokularfile;
2291 }
2292 
2293 QVector<KPluginMetaData> DocumentPrivate::availableGenerators()
2294 {
2295     static QVector<KPluginMetaData> result;
2296     if (result.isEmpty()) {
2297         result = KPluginMetaData::findPlugins(QStringLiteral("okular_generators"));
2298     }
2299     return result;
2300 }
2301 
2302 KPluginMetaData DocumentPrivate::generatorForMimeType(const QMimeType &type, QWidget *widget, const QVector<KPluginMetaData> &triedOffers)
2303 {
2304     // First try to find an exact match, and then look for more general ones (e. g. the plain text one)
2305     // Ideally we would rank these by "closeness", but that might be overdoing it
2306 
2307     const QVector<KPluginMetaData> available = availableGenerators();
2308     QVector<KPluginMetaData> offers;
2309     QVector<KPluginMetaData> exactMatches;
2310 
2311     QMimeDatabase mimeDatabase;
2312 
2313     for (const KPluginMetaData &md : available) {
2314         if (triedOffers.contains(md)) {
2315             continue;
2316         }
2317 
2318         const QStringList mimetypes = md.mimeTypes();
2319         for (const QString &supported : mimetypes) {
2320             QMimeType mimeType = mimeDatabase.mimeTypeForName(supported);
2321             if (mimeType == type && !exactMatches.contains(md)) {
2322                 exactMatches << md;
2323             }
2324 
2325             if (type.inherits(supported) && !offers.contains(md)) {
2326                 offers << md;
2327             }
2328         }
2329     }
2330 
2331     if (!exactMatches.isEmpty()) {
2332         offers = exactMatches;
2333     }
2334 
2335     if (offers.isEmpty()) {
2336         return KPluginMetaData();
2337     }
2338     int hRank = 0;
2339     // best ranked offer search
2340     int offercount = offers.size();
2341     if (offercount > 1) {
2342         // sort the offers: the offers with an higher priority come before
2343         auto cmp = [](const KPluginMetaData &s1, const KPluginMetaData &s2) {
2344             const QString property = QStringLiteral("X-KDE-Priority");
2345             return s1.rawData()[property].toInt() > s2.rawData()[property].toInt();
2346         };
2347         std::stable_sort(offers.begin(), offers.end(), cmp);
2348 
2349         if (SettingsCore::chooseGenerators()) {
2350             QStringList list;
2351             for (int i = 0; i < offercount; ++i) {
2352                 list << offers.at(i).pluginId();
2353             }
2354             ChooseEngineDialog choose(list, type, widget);
2355 
2356             if (choose.exec() == QDialog::Rejected) {
2357                 return KPluginMetaData();
2358             }
2359 
2360             hRank = choose.selectedGenerator();
2361         }
2362     }
2363     Q_ASSERT(hRank < offers.size());
2364     return offers.at(hRank);
2365 }
2366 
2367 Document::OpenResult Document::openDocument(const QString &docFile, const QUrl &url, const QMimeType &_mime, const QString &password)
2368 {
2369     QMimeDatabase db;
2370     QMimeType mime = _mime;
2371     QByteArray filedata;
2372     int fd = -1;
2373     if (url.scheme() == QLatin1String("fd")) {
2374         bool ok;
2375         fd = QStringView {url.path()}.mid(1).toInt(&ok);
2376         if (!ok) {
2377             return OpenError;
2378         }
2379     } else if (url.fileName() == QLatin1String("-")) {
2380         fd = 0;
2381     }
2382     bool triedMimeFromFileContent = false;
2383     if (fd < 0) {
2384         if (!mime.isValid()) {
2385             return OpenError;
2386         }
2387 
2388         d->m_url = url;
2389         d->m_docFileName = docFile;
2390 
2391         if (!d->updateMetadataXmlNameAndDocSize()) {
2392             return OpenError;
2393         }
2394     } else {
2395         QFile qstdin;
2396         const bool ret = qstdin.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle);
2397         if (!ret) {
2398             qWarning() << "failed to read" << url << filedata;
2399             return OpenError;
2400         }
2401 
2402         filedata = qstdin.readAll();
2403         mime = db.mimeTypeForData(filedata);
2404         if (!mime.isValid() || mime.isDefault()) {
2405             return OpenError;
2406         }
2407         d->m_docSize = filedata.size();
2408         triedMimeFromFileContent = true;
2409     }
2410 
2411     const bool fromFileDescriptor = fd >= 0;
2412 
2413     // 0. load Generator
2414     // request only valid non-disabled plugins suitable for the mimetype
2415     KPluginMetaData offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2416     if (!offer.isValid() && !triedMimeFromFileContent) {
2417         QMimeType newmime = db.mimeTypeForFile(docFile, QMimeDatabase::MatchContent);
2418         triedMimeFromFileContent = true;
2419         if (newmime != mime) {
2420             mime = newmime;
2421             offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2422         }
2423         if (!offer.isValid()) {
2424             // There's still no offers, do a final mime search based on the filename
2425             // We need this because sometimes (e.g. when downloading from a webserver) the mimetype we
2426             // use is the one fed by the server, that may be wrong
2427             newmime = db.mimeTypeForUrl(url);
2428 
2429             if (!newmime.isDefault() && newmime != mime) {
2430                 mime = newmime;
2431                 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2432             }
2433         }
2434     }
2435     if (!offer.isValid()) {
2436         d->m_openError = i18n("Can not find a plugin which is able to handle the document being passed.");
2437         Q_EMIT error(d->m_openError, -1);
2438         qCWarning(OkularCoreDebug).nospace() << "No plugin for mimetype '" << mime.name() << "'.";
2439         return OpenError;
2440     }
2441 
2442     // 1. load Document
2443     OpenResult openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2444     if (openResult == OpenError) {
2445         QVector<KPluginMetaData> triedOffers;
2446         triedOffers << offer;
2447         offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2448 
2449         while (offer.isValid()) {
2450             openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2451 
2452             if (openResult == OpenError) {
2453                 triedOffers << offer;
2454                 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2455             } else {
2456                 break;
2457             }
2458         }
2459 
2460         if (openResult == OpenError && !triedMimeFromFileContent) {
2461             QMimeType newmime = db.mimeTypeForFile(docFile, QMimeDatabase::MatchContent);
2462             triedMimeFromFileContent = true;
2463             if (newmime != mime) {
2464                 mime = newmime;
2465                 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2466                 while (offer.isValid()) {
2467                     openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2468 
2469                     if (openResult == OpenError) {
2470                         triedOffers << offer;
2471                         offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2472                     } else {
2473                         break;
2474                     }
2475                 }
2476             }
2477         }
2478 
2479         if (openResult == OpenSuccess) {
2480             // Clear errors, since we're trying various generators, maybe one of them errored out
2481             // but we finally succeeded
2482             // TODO one can still see the error message animating out but since this is a very rare
2483             //      condition we can leave this for future work
2484             Q_EMIT error(QString(), -1);
2485         }
2486     }
2487     if (openResult != OpenSuccess) {
2488         return openResult;
2489     }
2490 
2491     // no need to check for the existence of a synctex file, no parser will be
2492     // created if none exists
2493     d->m_synctex_scanner = synctex_scanner_new_with_output_file(QFile::encodeName(docFile).constData(), nullptr, 1);
2494     if (!d->m_synctex_scanner && QFile::exists(docFile + QLatin1String("sync"))) {
2495         d->loadSyncFile(docFile);
2496     }
2497 
2498     d->m_generatorName = offer.pluginId();
2499     d->m_pageController = new PageController();
2500     connect(d->m_pageController, &PageController::rotationFinished, this, [this](int p, Okular::Page *op) { d->rotationFinished(p, op); });
2501 
2502     for (Page *p : std::as_const(d->m_pagesVector)) {
2503         p->d->m_doc = d;
2504     }
2505 
2506     d->m_metadataLoadingCompleted = false;
2507     d->m_docdataMigrationNeeded = false;
2508 
2509     // 2. load Additional Data (bookmarks, local annotations and metadata) about the document
2510     if (d->m_archiveData) {
2511         // QTemporaryFile is weird and will return false in exists if fileName wasn't called before
2512         d->m_archiveData->metadataFile.fileName();
2513         d->loadDocumentInfo(d->m_archiveData->metadataFile, LoadPageInfo);
2514         d->loadDocumentInfo(LoadGeneralInfo);
2515     } else {
2516         if (d->loadDocumentInfo(LoadPageInfo)) {
2517             d->m_docdataMigrationNeeded = true;
2518         }
2519         d->loadDocumentInfo(LoadGeneralInfo);
2520     }
2521 
2522     d->m_metadataLoadingCompleted = true;
2523     d->m_bookmarkManager->setUrl(d->m_url);
2524 
2525     // 3. setup observers internal lists and data
2526     foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged));
2527 
2528     // 4. set initial page (restoring the page saved in xml if loaded)
2529     DocumentViewport loadedViewport = (*d->m_viewportIterator);
2530     if (loadedViewport.isValid()) {
2531         (*d->m_viewportIterator) = DocumentViewport();
2532         if (loadedViewport.pageNumber >= (int)d->m_pagesVector.size()) {
2533             loadedViewport.pageNumber = d->m_pagesVector.size() - 1;
2534         }
2535     } else {
2536         loadedViewport.pageNumber = 0;
2537     }
2538     setViewport(loadedViewport);
2539 
2540     // start bookmark saver timer
2541     if (!d->m_saveBookmarksTimer) {
2542         d->m_saveBookmarksTimer = new QTimer(this);
2543         connect(d->m_saveBookmarksTimer, &QTimer::timeout, this, [this] { d->saveDocumentInfo(); });
2544     }
2545     d->m_saveBookmarksTimer->start(5 * 60 * 1000);
2546 
2547     // start memory check timer
2548     if (!d->m_memCheckTimer) {
2549         d->m_memCheckTimer = new QTimer(this);
2550         connect(d->m_memCheckTimer, &QTimer::timeout, this, [this] { d->slotTimedMemoryCheck(); });
2551     }
2552     d->m_memCheckTimer->start(kMemCheckTime);
2553 
2554     const DocumentViewport nextViewport = d->nextDocumentViewport();
2555     if (nextViewport.isValid()) {
2556         setViewport(nextViewport);
2557         d->m_nextDocumentViewport = DocumentViewport();
2558         d->m_nextDocumentDestination = QString();
2559     }
2560 
2561     AudioPlayer::instance()->setDocument(fromFileDescriptor ? QUrl() : d->m_url, this);
2562 
2563     const QStringList docScripts = d->m_generator->metaData(QStringLiteral("DocumentScripts"), QStringLiteral("JavaScript")).toStringList();
2564     if (!docScripts.isEmpty()) {
2565         d->m_scripter = new Scripter(d);
2566         for (const QString &docscript : docScripts) {
2567             d->m_scripter->execute(JavaScript, docscript);
2568         }
2569     }
2570 
2571     return OpenSuccess;
2572 }
2573 
2574 bool DocumentPrivate::updateMetadataXmlNameAndDocSize()
2575 {
2576     // m_docFileName is always local so we can use QFileInfo on it
2577     QFileInfo fileReadTest(m_docFileName);
2578     if (!fileReadTest.isFile() && !fileReadTest.isReadable()) {
2579         return false;
2580     }
2581 
2582     m_docSize = fileReadTest.size();
2583 
2584     // determine the related "xml document-info" filename
2585     if (m_url.isLocalFile()) {
2586         const QString filePath = docDataFileName(m_url, m_docSize);
2587         qCDebug(OkularCoreDebug) << "Metadata file is now:" << filePath;
2588         m_xmlFileName = filePath;
2589     } else {
2590         qCDebug(OkularCoreDebug) << "Metadata file: disabled";
2591         m_xmlFileName = QString();
2592     }
2593 
2594     return true;
2595 }
2596 
2597 KXMLGUIClient *Document::guiClient()
2598 {
2599     if (d->m_generator) {
2600         Okular::GuiInterface *iface = qobject_cast<Okular::GuiInterface *>(d->m_generator);
2601         if (iface) {
2602             return iface->guiClient();
2603         }
2604     }
2605     return nullptr;
2606 }
2607 
2608 void Document::closeDocument()
2609 {
2610     // check if there's anything to close...
2611     if (!d->m_generator) {
2612         return;
2613     }
2614 
2615     Q_EMIT aboutToClose();
2616 
2617     delete d->m_pageController;
2618     d->m_pageController = nullptr;
2619 
2620     delete d->m_scripter;
2621     d->m_scripter = nullptr;
2622 
2623     // remove requests left in queue
2624     d->clearAndWaitForRequests();
2625 
2626     if (d->m_fontThread) {
2627         disconnect(d->m_fontThread, nullptr, this, nullptr);
2628         d->m_fontThread->stopExtraction();
2629         d->m_fontThread->wait();
2630         d->m_fontThread = nullptr;
2631     }
2632 
2633     // stop any audio playback
2634     AudioPlayer::instance()->stopPlaybacks();
2635 
2636     // close the current document and save document info if a document is still opened
2637     if (d->m_generator && d->m_pagesVector.size() > 0) {
2638         d->saveDocumentInfo();
2639 
2640         // free the content of the opaque backend actions (if any)
2641         // this is a bit awkward since backends can store "random stuff" in the
2642         // BackendOpaqueAction nativeId qvariant so we need to tell them to free it
2643         // ideally we would just do that in the BackendOpaqueAction destructor
2644         // but that's too late in the cleanup process, i.e. the generator has already closed its document
2645         // and the document generator is nullptr
2646         for (Page *p : std::as_const(d->m_pagesVector)) {
2647             const QList<ObjectRect *> &oRects = p->objectRects();
2648             for (ObjectRect *oRect : oRects) {
2649                 if (oRect->objectType() == ObjectRect::Action) {
2650                     const Action *a = static_cast<const Action *>(oRect->object());
2651                     const BackendOpaqueAction *backendAction = dynamic_cast<const BackendOpaqueAction *>(a);
2652                     if (backendAction) {
2653                         d->m_generator->freeOpaqueActionContents(*backendAction);
2654                     }
2655                 }
2656             }
2657 
2658             const QList<FormField *> forms = p->formFields();
2659             for (const FormField *form : forms) {
2660                 const QList<Action *> additionalActions = form->additionalActions();
2661                 for (const Action *a : additionalActions) {
2662                     const BackendOpaqueAction *backendAction = dynamic_cast<const BackendOpaqueAction *>(a);
2663                     if (backendAction) {
2664                         d->m_generator->freeOpaqueActionContents(*backendAction);
2665                     }
2666                 }
2667             }
2668         }
2669 
2670         d->m_generator->closeDocument();
2671     }
2672 
2673     if (d->m_synctex_scanner) {
2674         synctex_scanner_free(d->m_synctex_scanner);
2675         d->m_synctex_scanner = nullptr;
2676     }
2677 
2678     // stop timers
2679     if (d->m_memCheckTimer) {
2680         d->m_memCheckTimer->stop();
2681     }
2682     if (d->m_saveBookmarksTimer) {
2683         d->m_saveBookmarksTimer->stop();
2684     }
2685 
2686     if (d->m_generator) {
2687         // disconnect the generator from this document ...
2688         d->m_generator->d_func()->m_document = nullptr;
2689         // .. and this document from the generator signals
2690         disconnect(d->m_generator, nullptr, this, nullptr);
2691 
2692         QHash<QString, GeneratorInfo>::const_iterator genIt = d->m_loadedGenerators.constFind(d->m_generatorName);
2693         Q_ASSERT(genIt != d->m_loadedGenerators.constEnd());
2694     }
2695     d->m_generator = nullptr;
2696     d->m_generatorName = QString();
2697     d->m_url = QUrl();
2698     d->m_walletGenerator = nullptr;
2699     d->m_docFileName = QString();
2700     d->m_xmlFileName = QString();
2701     delete d->m_tempFile;
2702     d->m_tempFile = nullptr;
2703     delete d->m_archiveData;
2704     d->m_archiveData = nullptr;
2705     d->m_docSize = -1;
2706     d->m_exportCached = false;
2707     d->m_exportFormats.clear();
2708     d->m_exportToText = ExportFormat();
2709     d->m_fontsCached = false;
2710     d->m_fontsCache.clear();
2711     d->m_rotation = Rotation0;
2712 
2713     // send an empty list to observers (to free their data)
2714     foreachObserver(notifySetup(QVector<Page *>(), DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged));
2715 
2716     // delete pages and clear 'd->m_pagesVector' container
2717     QVector<Page *>::const_iterator pIt = d->m_pagesVector.constBegin();
2718     QVector<Page *>::const_iterator pEnd = d->m_pagesVector.constEnd();
2719     for (; pIt != pEnd; ++pIt) {
2720         delete *pIt;
2721     }
2722     d->m_pagesVector.clear();
2723 
2724     // clear 'memory allocation' descriptors
2725     qDeleteAll(d->m_allocatedPixmaps);
2726     d->m_allocatedPixmaps.clear();
2727 
2728     // clear 'running searches' descriptors
2729     QMap<int, RunningSearch *>::const_iterator rIt = d->m_searches.constBegin();
2730     QMap<int, RunningSearch *>::const_iterator rEnd = d->m_searches.constEnd();
2731     for (; rIt != rEnd; ++rIt) {
2732         delete *rIt;
2733     }
2734     d->m_searches.clear();
2735 
2736     // clear the visible areas and notify the observers
2737     QVector<VisiblePageRect *>::const_iterator vIt = d->m_pageRects.constBegin();
2738     QVector<VisiblePageRect *>::const_iterator vEnd = d->m_pageRects.constEnd();
2739     for (; vIt != vEnd; ++vIt) {
2740         delete *vIt;
2741     }
2742     d->m_pageRects.clear();
2743     foreachObserver(notifyVisibleRectsChanged());
2744 
2745     // reset internal variables
2746 
2747     d->m_viewportHistory.clear();
2748     d->m_viewportHistory.emplace_back();
2749     d->m_viewportIterator = d->m_viewportHistory.begin();
2750     d->m_allocatedPixmapsTotalMemory = 0;
2751     d->m_allocatedTextPagesFifo.clear();
2752     d->m_pageSize = PageSize();
2753     d->m_pageSizes.clear();
2754 
2755     d->m_documentInfo = DocumentInfo();
2756     d->m_documentInfoAskedKeys.clear();
2757 
2758     AudioPlayer::instance()->resetDocument();
2759 
2760     d->m_undoStack->clear();
2761     d->m_docdataMigrationNeeded = false;
2762 
2763 #if HAVE_MALLOC_TRIM
2764     // trim unused memory, glibc should do this but it seems it does not
2765     // this can greatly decrease the [perceived] memory consumption of okular
2766     // see: https://sourceware.org/bugzilla/show_bug.cgi?id=14827
2767     malloc_trim(0);
2768 #endif
2769 }
2770 
2771 void Document::addObserver(DocumentObserver *pObserver)
2772 {
2773     Q_ASSERT(!d->m_observers.contains(pObserver));
2774     d->m_observers << pObserver;
2775 
2776     // if the observer is added while a document is already opened, tell it
2777     if (!d->m_pagesVector.isEmpty()) {
2778         pObserver->notifySetup(d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged);
2779         pObserver->notifyViewportChanged(false /*disables smoothMove*/);
2780     }
2781 }
2782 
2783 void Document::removeObserver(DocumentObserver *pObserver)
2784 {
2785     // remove observer from the set. it won't receive notifications anymore
2786     if (d->m_observers.contains(pObserver)) {
2787         // free observer's pixmap data
2788         QVector<Page *>::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd();
2789         for (; it != end; ++it) {
2790             (*it)->deletePixmap(pObserver);
2791         }
2792 
2793         // [MEM] free observer's allocation descriptors
2794         std::list<AllocatedPixmap *>::iterator aIt = d->m_allocatedPixmaps.begin();
2795         std::list<AllocatedPixmap *>::iterator aEnd = d->m_allocatedPixmaps.end();
2796         while (aIt != aEnd) {
2797             AllocatedPixmap *p = *aIt;
2798             if (p->observer == pObserver) {
2799                 aIt = d->m_allocatedPixmaps.erase(aIt);
2800                 delete p;
2801             } else {
2802                 ++aIt;
2803             }
2804         }
2805 
2806         for (PixmapRequest *executingRequest : std::as_const(d->m_executingPixmapRequests)) {
2807             if (executingRequest->observer() == pObserver) {
2808                 d->cancelRenderingBecauseOf(executingRequest, nullptr);
2809             }
2810         }
2811 
2812         // remove observer entry from the set
2813         d->m_observers.remove(pObserver);
2814     }
2815 }
2816 
2817 void Document::reparseConfig()
2818 {
2819     // reparse generator config and if something changed clear Pages
2820     bool configchanged = false;
2821     if (d->m_generator) {
2822         Okular::ConfigInterface *iface = qobject_cast<Okular::ConfigInterface *>(d->m_generator);
2823         if (iface) {
2824             configchanged = iface->reparseConfig();
2825         }
2826     }
2827     if (configchanged) {
2828         // invalidate pixmaps
2829         QVector<Page *>::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd();
2830         for (; it != end; ++it) {
2831             (*it)->deletePixmaps();
2832         }
2833 
2834         // [MEM] remove allocation descriptors
2835         qDeleteAll(d->m_allocatedPixmaps);
2836         d->m_allocatedPixmaps.clear();
2837         d->m_allocatedPixmapsTotalMemory = 0;
2838 
2839         // send reload signals to observers
2840         foreachObserver(notifyContentsCleared(DocumentObserver::Pixmap));
2841     }
2842 
2843     // free memory if in 'low' profile
2844     if (SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !d->m_allocatedPixmaps.empty() && !d->m_pagesVector.isEmpty()) {
2845         d->cleanupPixmapMemory();
2846     }
2847 }
2848 
2849 bool Document::isOpened() const
2850 {
2851     return d->m_generator;
2852 }
2853 
2854 bool Document::canConfigurePrinter() const
2855 {
2856     if (d->m_generator) {
2857         Okular::PrintInterface *iface = qobject_cast<Okular::PrintInterface *>(d->m_generator);
2858         return iface ? true : false;
2859     } else {
2860         return false;
2861     }
2862 }
2863 
2864 bool Document::sign(const NewSignatureData &data, const QString &newPath)
2865 {
2866     if (d->m_generator->canSign()) {
2867         return d->m_generator->sign(data, newPath);
2868     } else {
2869         return false;
2870     }
2871 }
2872 
2873 Okular::CertificateStore *Document::certificateStore() const
2874 {
2875     return d->m_generator ? d->m_generator->certificateStore() : nullptr;
2876 }
2877 
2878 void Document::setEditorCommandOverride(const QString &editCmd)
2879 {
2880     d->editorCommandOverride = editCmd;
2881 }
2882 
2883 QString Document::editorCommandOverride() const
2884 {
2885     return d->editorCommandOverride;
2886 }
2887 
2888 DocumentInfo Document::documentInfo() const
2889 {
2890     QSet<DocumentInfo::Key> keys;
2891     for (Okular::DocumentInfo::Key ks = Okular::DocumentInfo::Title; ks < Okular::DocumentInfo::Invalid; ks = Okular::DocumentInfo::Key(ks + 1)) {
2892         keys << ks;
2893     }
2894 
2895     return documentInfo(keys);
2896 }
2897 
2898 DocumentInfo Document::documentInfo(const QSet<DocumentInfo::Key> &keys) const
2899 {
2900     DocumentInfo result = d->m_documentInfo;
2901     const QSet<DocumentInfo::Key> missingKeys = keys - d->m_documentInfoAskedKeys;
2902 
2903     if (d->m_generator && !missingKeys.isEmpty()) {
2904         DocumentInfo info = d->m_generator->generateDocumentInfo(missingKeys);
2905 
2906         if (missingKeys.contains(DocumentInfo::FilePath)) {
2907             info.set(DocumentInfo::FilePath, currentDocument().toDisplayString());
2908         }
2909 
2910         if (d->m_docSize != -1 && missingKeys.contains(DocumentInfo::DocumentSize)) {
2911             const QString sizeString = KFormat().formatByteSize(d->m_docSize);
2912             info.set(DocumentInfo::DocumentSize, sizeString);
2913         }
2914         if (missingKeys.contains(DocumentInfo::PagesSize)) {
2915             const QString pagesSize = d->pagesSizeString();
2916             if (!pagesSize.isEmpty()) {
2917                 info.set(DocumentInfo::PagesSize, pagesSize);
2918             }
2919         }
2920 
2921         if (missingKeys.contains(DocumentInfo::Pages) && info.get(DocumentInfo::Pages).isEmpty()) {
2922             info.set(DocumentInfo::Pages, QString::number(this->pages()));
2923         }
2924 
2925         d->m_documentInfo.d->values.insert(info.d->values);
2926         d->m_documentInfo.d->titles.insert(info.d->titles);
2927         result.d->values.insert(info.d->values);
2928         result.d->titles.insert(info.d->titles);
2929     }
2930     d->m_documentInfoAskedKeys += keys;
2931 
2932     return result;
2933 }
2934 
2935 const DocumentSynopsis *Document::documentSynopsis() const
2936 {
2937     return d->m_generator ? d->m_generator->generateDocumentSynopsis() : nullptr;
2938 }
2939 
2940 void Document::startFontReading()
2941 {
2942     if (!d->m_generator || !d->m_generator->hasFeature(Generator::FontInfo) || d->m_fontThread) {
2943         return;
2944     }
2945 
2946     if (d->m_fontsCached) {
2947         // in case we have cached fonts, simulate a reading
2948         // this way the API is the same, and users no need to care about the
2949         // internal caching
2950         for (int i = 0; i < d->m_fontsCache.count(); ++i) {
2951             Q_EMIT gotFont(d->m_fontsCache.at(i));
2952             Q_EMIT fontReadingProgress(i / pages());
2953         }
2954         Q_EMIT fontReadingEnded();
2955         return;
2956     }
2957 
2958     d->m_fontThread = new FontExtractionThread(d->m_generator, pages());
2959     connect(d->m_fontThread, &FontExtractionThread::gotFont, this, [this](const Okular::FontInfo &f) { d->fontReadingGotFont(f); });
2960     connect(d->m_fontThread.data(), &FontExtractionThread::progress, this, [this](int p) { d->slotFontReadingProgress(p); });
2961 
2962     d->m_fontThread->startExtraction(/*d->m_generator->hasFeature( Generator::Threaded )*/ true);
2963 }
2964 
2965 void Document::stopFontReading()
2966 {
2967     if (!d->m_fontThread) {
2968         return;
2969     }
2970 
2971     disconnect(d->m_fontThread, nullptr, this, nullptr);
2972     d->m_fontThread->stopExtraction();
2973     d->m_fontThread = nullptr;
2974     d->m_fontsCache.clear();
2975 }
2976 
2977 bool Document::canProvideFontInformation() const
2978 {
2979     return d->m_generator ? d->m_generator->hasFeature(Generator::FontInfo) : false;
2980 }
2981 
2982 bool Document::canSign() const
2983 {
2984     return d->m_generator ? d->m_generator->canSign() : false;
2985 }
2986 
2987 const QList<EmbeddedFile *> *Document::embeddedFiles() const
2988 {
2989     return d->m_generator ? d->m_generator->embeddedFiles() : nullptr;
2990 }
2991 
2992 const Page *Document::page(int n) const
2993 {
2994     return (n >= 0 && n < d->m_pagesVector.count()) ? d->m_pagesVector.at(n) : nullptr;
2995 }
2996 
2997 const DocumentViewport &Document::viewport() const
2998 {
2999     return (*d->m_viewportIterator);
3000 }
3001 
3002 const QVector<VisiblePageRect *> &Document::visiblePageRects() const
3003 {
3004     return d->m_pageRects;
3005 }
3006 
3007 void Document::setVisiblePageRects(const QVector<VisiblePageRect *> &visiblePageRects, DocumentObserver *excludeObserver)
3008 {
3009     QVector<VisiblePageRect *>::const_iterator vIt = d->m_pageRects.constBegin();
3010     QVector<VisiblePageRect *>::const_iterator vEnd = d->m_pageRects.constEnd();
3011     for (; vIt != vEnd; ++vIt) {
3012         delete *vIt;
3013     }
3014     d->m_pageRects = visiblePageRects;
3015     // notify change to all other (different from id) observers
3016     for (DocumentObserver *o : std::as_const(d->m_observers)) {
3017         if (o != excludeObserver) {
3018             o->notifyVisibleRectsChanged();
3019         }
3020     }
3021 }
3022 
3023 uint Document::currentPage() const
3024 {
3025     return (*d->m_viewportIterator).pageNumber;
3026 }
3027 
3028 uint Document::pages() const
3029 {
3030     return d->m_pagesVector.size();
3031 }
3032 
3033 QUrl Document::currentDocument() const
3034 {
3035     return d->m_url;
3036 }
3037 
3038 bool Document::isAllowed(Permission action) const
3039 {
3040     if (action == Okular::AllowNotes && (d->m_docdataMigrationNeeded || !d->m_annotationEditingEnabled)) {
3041         return false;
3042     }
3043     if (action == Okular::AllowFillForms && d->m_docdataMigrationNeeded) {
3044         return false;
3045     }
3046 
3047 #if !OKULAR_FORCE_DRM
3048     if (KAuthorized::authorize(QStringLiteral("skip_drm")) && !SettingsCore::obeyDRM()) {
3049         return true;
3050     }
3051 #endif
3052 
3053     return d->m_generator ? d->m_generator->isAllowed(action) : false;
3054 }
3055 
3056 bool Document::supportsSearching() const
3057 {
3058     return d->m_generator ? d->m_generator->hasFeature(Generator::TextExtraction) : false;
3059 }
3060 
3061 bool Document::supportsPageSizes() const
3062 {
3063     return d->m_generator ? d->m_generator->hasFeature(Generator::PageSizes) : false;
3064 }
3065 
3066 bool Document::supportsTiles() const
3067 {
3068     return d->m_generator ? d->m_generator->hasFeature(Generator::TiledRendering) : false;
3069 }
3070 
3071 PageSize::List Document::pageSizes() const
3072 {
3073     if (d->m_generator) {
3074         if (d->m_pageSizes.isEmpty()) {
3075             d->m_pageSizes = d->m_generator->pageSizes();
3076         }
3077         return d->m_pageSizes;
3078     }
3079     return PageSize::List();
3080 }
3081 
3082 bool Document::canExportToText() const
3083 {
3084     if (!d->m_generator) {
3085         return false;
3086     }
3087 
3088     d->cacheExportFormats();
3089     return !d->m_exportToText.isNull();
3090 }
3091 
3092 bool Document::exportToText(const QString &fileName) const
3093 {
3094     if (!d->m_generator) {
3095         return false;
3096     }
3097 
3098     d->cacheExportFormats();
3099     if (d->m_exportToText.isNull()) {
3100         return false;
3101     }
3102 
3103     return d->m_generator->exportTo(fileName, d->m_exportToText);
3104 }
3105 
3106 ExportFormat::List Document::exportFormats() const
3107 {
3108     if (!d->m_generator) {
3109         return ExportFormat::List();
3110     }
3111 
3112     d->cacheExportFormats();
3113     return d->m_exportFormats;
3114 }
3115 
3116 bool Document::exportTo(const QString &fileName, const ExportFormat &format) const
3117 {
3118     return d->m_generator ? d->m_generator->exportTo(fileName, format) : false;
3119 }
3120 
3121 bool Document::historyAtBegin() const
3122 {
3123     return d->m_viewportIterator == d->m_viewportHistory.begin();
3124 }
3125 
3126 bool Document::historyAtEnd() const
3127 {
3128     return d->m_viewportIterator == --(d->m_viewportHistory.end());
3129 }
3130 
3131 QVariant Document::metaData(const QString &key, const QVariant &option) const
3132 {
3133     // if option starts with "src:" assume that we are handling a
3134     // source reference
3135     if (key == QLatin1String("NamedViewport") && option.toString().startsWith(QLatin1String("src:"), Qt::CaseInsensitive) && d->m_synctex_scanner) {
3136         const QString reference = option.toString();
3137 
3138         // The reference is of form "src:1111Filename", where "1111"
3139         // points to line number 1111 in the file "Filename".
3140         // Extract the file name and the numeral part from the reference string.
3141         // This will fail if Filename starts with a digit.
3142         QString name, lineString;
3143         // Remove "src:". Presence of substring has been checked before this
3144         // function is called.
3145         name = reference.mid(4);
3146         // split
3147         int nameLength = name.length();
3148         int i = 0;
3149         for (i = 0; i < nameLength; ++i) {
3150             if (!name[i].isDigit()) {
3151                 break;
3152             }
3153         }
3154         lineString = name.left(i);
3155         name = name.mid(i);
3156         // Remove spaces.
3157         name = name.trimmed();
3158         lineString = lineString.trimmed();
3159         // Convert line to integer.
3160         bool ok;
3161         int line = lineString.toInt(&ok);
3162         if (!ok) {
3163             line = -1;
3164         }
3165 
3166         // Use column == -1 for now.
3167         if (synctex_display_query(d->m_synctex_scanner, QFile::encodeName(name).constData(), line, -1, 0) > 0) {
3168             synctex_node_p node;
3169             // For now use the first hit. Could possibly be made smarter
3170             // in case there are multiple hits.
3171             while ((node = synctex_scanner_next_result(d->m_synctex_scanner))) {
3172                 Okular::DocumentViewport viewport;
3173 
3174                 // TeX pages start at 1.
3175                 viewport.pageNumber = synctex_node_page(node) - 1;
3176 
3177                 if (viewport.pageNumber >= 0) {
3178                     const QSizeF dpi = d->m_generator->dpi();
3179 
3180                     // TeX small points ...
3181                     double px = (synctex_node_visible_h(node) * dpi.width()) / 72.27;
3182                     double py = (synctex_node_visible_v(node) * dpi.height()) / 72.27;
3183                     viewport.rePos.normalizedX = px / page(viewport.pageNumber)->width();
3184                     viewport.rePos.normalizedY = (py + 0.5) / page(viewport.pageNumber)->height();
3185                     viewport.rePos.enabled = true;
3186                     viewport.rePos.pos = Okular::DocumentViewport::Center;
3187 
3188                     return viewport.toString();
3189                 }
3190             }
3191         }
3192     }
3193     return d->m_generator ? d->m_generator->metaData(key, option) : QVariant();
3194 }
3195 
3196 Rotation Document::rotation() const
3197 {
3198     return d->m_rotation;
3199 }
3200 
3201 QSizeF Document::allPagesSize() const
3202 {
3203     bool allPagesSameSize = true;
3204     QSizeF size;
3205     for (int i = 0; allPagesSameSize && i < d->m_pagesVector.count(); ++i) {
3206         const Page *p = d->m_pagesVector.at(i);
3207         if (i == 0) {
3208             size = QSizeF(p->width(), p->height());
3209         } else {
3210             allPagesSameSize = (size == QSizeF(p->width(), p->height()));
3211         }
3212     }
3213     if (allPagesSameSize) {
3214         return size;
3215     } else {
3216         return QSizeF();
3217     }
3218 }
3219 
3220 QString Document::pageSizeString(int page) const
3221 {
3222     if (d->m_generator) {
3223         if (d->m_generator->pagesSizeMetric() != Generator::None) {
3224             const Page *p = d->m_pagesVector.at(page);
3225             return d->localizedSize(QSizeF(p->width(), p->height()));
3226         }
3227     }
3228     return QString();
3229 }
3230 
3231 static bool shouldCancelRenderingBecauseOf(const PixmapRequest &executingRequest, const PixmapRequest &otherRequest)
3232 {
3233     // New request has higher priority -> cancel
3234     if (executingRequest.priority() > otherRequest.priority()) {
3235         return true;
3236     }
3237 
3238     // New request has lower priority -> don't cancel
3239     if (executingRequest.priority() < otherRequest.priority()) {
3240         return false;
3241     }
3242 
3243     // New request has same priority and is from a different observer -> don't cancel
3244     // AFAIK this never happens since all observers have different priorities
3245     if (executingRequest.observer() != otherRequest.observer()) {
3246         return false;
3247     }
3248 
3249     // Same priority and observer, different page number -> don't cancel
3250     // may still end up cancelled later in the parent caller if none of the requests
3251     // is of the executingRequest page and RemoveAllPrevious is specified
3252     if (executingRequest.pageNumber() != otherRequest.pageNumber()) {
3253         return false;
3254     }
3255 
3256     // Same priority, observer, page, different size -> cancel
3257     if (executingRequest.width() != otherRequest.width()) {
3258         return true;
3259     }
3260 
3261     // Same priority, observer, page, different size -> cancel
3262     if (executingRequest.height() != otherRequest.height()) {
3263         return true;
3264     }
3265 
3266     // Same priority, observer, page, different tiling -> cancel
3267     if (executingRequest.isTile() != otherRequest.isTile()) {
3268         return true;
3269     }
3270 
3271     // Same priority, observer, page, different tiling -> cancel
3272     if (executingRequest.isTile()) {
3273         const NormalizedRect bothRequestsRect = executingRequest.normalizedRect() | otherRequest.normalizedRect();
3274         if (!(bothRequestsRect == executingRequest.normalizedRect())) {
3275             return true;
3276         }
3277     }
3278 
3279     return false;
3280 }
3281 
3282 bool DocumentPrivate::cancelRenderingBecauseOf(PixmapRequest *executingRequest, PixmapRequest *newRequest)
3283 {
3284     // No point in aborting the rendering already finished, let it go through
3285     if (!executingRequest->d->mResultImage.isNull()) {
3286         return false;
3287     }
3288 
3289     if (newRequest && newRequest->asynchronous() && executingRequest->partialUpdatesWanted()) {
3290         newRequest->setPartialUpdatesWanted(true);
3291     }
3292 
3293     TilesManager *tm = executingRequest->d->tilesManager();
3294     if (tm) {
3295         tm->setPixmap(nullptr, executingRequest->normalizedRect(), true /*isPartialPixmap*/);
3296         tm->setRequest(NormalizedRect(), 0, 0);
3297     }
3298     PagePrivate::PixmapObject object = executingRequest->page()->d->m_pixmaps.take(executingRequest->observer());
3299     delete object.m_pixmap;
3300 
3301     if (executingRequest->d->mShouldAbortRender != 0) {
3302         return false;
3303     }
3304 
3305     executingRequest->d->mShouldAbortRender = 1;
3306 
3307     if (m_generator->d_ptr->mTextPageGenerationThread && m_generator->d_ptr->mTextPageGenerationThread->page() == executingRequest->page()) {
3308         m_generator->d_ptr->mTextPageGenerationThread->abortExtraction();
3309     }
3310 
3311     return true;
3312 }
3313 
3314 void Document::requestPixmaps(const QList<PixmapRequest *> &requests)
3315 {
3316     requestPixmaps(requests, RemoveAllPrevious);
3317 }
3318 
3319 void Document::requestPixmaps(const QList<PixmapRequest *> &requests, PixmapRequestFlags reqOptions)
3320 {
3321     if (requests.isEmpty()) {
3322         return;
3323     }
3324 
3325     if (!d->m_pageController) {
3326         // delete requests..
3327         qDeleteAll(requests);
3328         // ..and return
3329         return;
3330     }
3331 
3332     QSet<DocumentObserver *> observersPixmapCleared;
3333 
3334     // 1. [CLEAN STACK] remove previous requests of requesterID
3335     DocumentObserver *requesterObserver = requests.first()->observer();
3336     QSet<int> requestedPages;
3337     {
3338         for (PixmapRequest *request : requests) {
3339             Q_ASSERT(request->observer() == requesterObserver);
3340             requestedPages.insert(request->pageNumber());
3341         }
3342     }
3343     const bool removeAllPrevious = reqOptions & RemoveAllPrevious;
3344     d->m_pixmapRequestsMutex.lock();
3345     std::list<PixmapRequest *>::iterator sIt = d->m_pixmapRequestsStack.begin(), sEnd = d->m_pixmapRequestsStack.end();
3346     while (sIt != sEnd) {
3347         if ((*sIt)->observer() == requesterObserver && (removeAllPrevious || requestedPages.contains((*sIt)->pageNumber()))) {
3348             // delete request and remove it from stack
3349             delete *sIt;
3350             sIt = d->m_pixmapRequestsStack.erase(sIt);
3351         } else {
3352             ++sIt;
3353         }
3354     }
3355 
3356     // 1.B [PREPROCESS REQUESTS] tweak some values of the requests
3357     for (PixmapRequest *request : requests) {
3358         // set the 'page field' (see PixmapRequest) and check if it is valid
3359         qCDebug(OkularCoreDebug).nospace() << "request observer=" << request->observer() << " " << request->width() << "x" << request->height() << "@" << request->pageNumber();
3360         if (d->m_pagesVector.value(request->pageNumber()) == nullptr) {
3361             // skip requests referencing an invalid page (must not happen)
3362             delete request;
3363             continue;
3364         }
3365 
3366         request->d->mPage = d->m_pagesVector.value(request->pageNumber());
3367 
3368         if (request->isTile()) {
3369             // Change the current request rect so that only invalid tiles are
3370             // requested. Also make sure the rect is tile-aligned.
3371             NormalizedRect tilesRect;
3372             const QList<Tile> tiles = request->d->tilesManager()->tilesAt(request->normalizedRect(), TilesManager::TerminalTile);
3373             QList<Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
3374             while (tIt != tEnd) {
3375                 const Tile &tile = *tIt;
3376                 if (!tile.isValid()) {
3377                     if (tilesRect.isNull()) {
3378                         tilesRect = tile.rect();
3379                     } else {
3380                         tilesRect |= tile.rect();
3381                     }
3382                 }
3383 
3384                 tIt++;
3385             }
3386 
3387             request->setNormalizedRect(tilesRect);
3388         }
3389 
3390         if (!request->asynchronous()) {
3391             request->d->mPriority = 0;
3392         }
3393     }
3394 
3395     // 1.C [CANCEL REQUESTS] cancel those requests that are running and should be cancelled because of the new requests coming in
3396     if (d->m_generator->hasFeature(Generator::SupportsCancelling)) {
3397         for (PixmapRequest *executingRequest : std::as_const(d->m_executingPixmapRequests)) {
3398             bool newRequestsContainExecutingRequestPage = false;
3399             bool requestCancelled = false;
3400             for (PixmapRequest *newRequest : requests) {
3401                 if (newRequest->pageNumber() == executingRequest->pageNumber() && requesterObserver == executingRequest->observer()) {
3402                     newRequestsContainExecutingRequestPage = true;
3403                 }
3404 
3405                 if (shouldCancelRenderingBecauseOf(*executingRequest, *newRequest)) {
3406                     requestCancelled = d->cancelRenderingBecauseOf(executingRequest, newRequest);
3407                 }
3408             }
3409 
3410             // If we were told to remove all the previous requests and the executing request page is not part of the new requests, cancel it
3411             if (!requestCancelled && removeAllPrevious && requesterObserver == executingRequest->observer() && !newRequestsContainExecutingRequestPage) {
3412                 requestCancelled = d->cancelRenderingBecauseOf(executingRequest, nullptr);
3413             }
3414 
3415             if (requestCancelled) {
3416                 observersPixmapCleared << executingRequest->observer();
3417             }
3418         }
3419     }
3420 
3421     // 2. [ADD TO STACK] add requests to stack
3422     for (PixmapRequest *request : requests) {
3423         // add request to the 'stack' at the right place
3424         if (!request->priority()) {
3425             // add priority zero requests to the top of the stack
3426             d->m_pixmapRequestsStack.push_back(request);
3427         } else {
3428             // insert in stack sorted by priority
3429             sIt = d->m_pixmapRequestsStack.begin();
3430             sEnd = d->m_pixmapRequestsStack.end();
3431             while (sIt != sEnd && (*sIt)->priority() > request->priority()) {
3432                 ++sIt;
3433             }
3434             d->m_pixmapRequestsStack.insert(sIt, request);
3435         }
3436     }
3437     d->m_pixmapRequestsMutex.unlock();
3438 
3439     // 3. [START FIRST GENERATION] if <NO>generator is ready, start a new generation,
3440     // or else (if gen is running) it will be started when the new contents will
3441     // come from generator (in requestDone())</NO>
3442     // all handling of requests put into sendGeneratorPixmapRequest
3443     //    if ( generator->canRequestPixmap() )
3444     d->sendGeneratorPixmapRequest();
3445 
3446     for (DocumentObserver *o : std::as_const(observersPixmapCleared)) {
3447         o->notifyContentsCleared(Okular::DocumentObserver::Pixmap);
3448     }
3449 }
3450 
3451 void Document::requestTextPage(uint pageNumber)
3452 {
3453     Page *kp = d->m_pagesVector[pageNumber];
3454     if (!d->m_generator || !kp) {
3455         return;
3456     }
3457 
3458     // Memory management for TextPages
3459 
3460     d->m_generator->generateTextPage(kp);
3461 }
3462 
3463 void DocumentPrivate::notifyAnnotationChanges(int page)
3464 {
3465     foreachObserverD(notifyPageChanged(page, DocumentObserver::Annotations));
3466 }
3467 
3468 void DocumentPrivate::notifyFormChanges(int /*page*/)
3469 {
3470     recalculateForms();
3471 }
3472 
3473 void Document::addPageAnnotation(int page, Annotation *annotation)
3474 {
3475     // Transform annotation's base boundary rectangle into unrotated coordinates
3476     Page *p = d->m_pagesVector[page];
3477     QTransform t = p->d->rotationMatrix();
3478     annotation->d_ptr->baseTransform(t.inverted());
3479     QUndoCommand *uc = new AddAnnotationCommand(this->d, annotation, page);
3480     d->m_undoStack->push(uc);
3481 }
3482 
3483 bool Document::canModifyPageAnnotation(const Annotation *annotation) const
3484 {
3485     if (!annotation || (annotation->flags() & Annotation::DenyWrite)) {
3486         return false;
3487     }
3488 
3489     if (!isAllowed(Okular::AllowNotes)) {
3490         return false;
3491     }
3492 
3493     if ((annotation->flags() & Annotation::External) && !d->canModifyExternalAnnotations()) {
3494         return false;
3495     }
3496 
3497     switch (annotation->subType()) {
3498     case Annotation::AText:
3499     case Annotation::ALine:
3500     case Annotation::AGeom:
3501     case Annotation::AHighlight:
3502     case Annotation::AStamp:
3503     case Annotation::AInk:
3504         return true;
3505     default:
3506         return false;
3507     }
3508 }
3509 
3510 void Document::prepareToModifyAnnotationProperties(Annotation *annotation)
3511 {
3512     Q_ASSERT(d->m_prevPropsOfAnnotBeingModified.isNull());
3513     if (!d->m_prevPropsOfAnnotBeingModified.isNull()) {
3514         qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties has already been called since last call to Document::modifyPageAnnotationProperties";
3515         return;
3516     }
3517     d->m_prevPropsOfAnnotBeingModified = annotation->getAnnotationPropertiesDomNode();
3518 }
3519 
3520 void Document::modifyPageAnnotationProperties(int page, Annotation *annotation)
3521 {
3522     Q_ASSERT(!d->m_prevPropsOfAnnotBeingModified.isNull());
3523     if (d->m_prevPropsOfAnnotBeingModified.isNull()) {
3524         qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties must be called before Annotation is modified";
3525         return;
3526     }
3527     QDomNode prevProps = d->m_prevPropsOfAnnotBeingModified;
3528     QUndoCommand *uc = new Okular::ModifyAnnotationPropertiesCommand(d, annotation, page, prevProps, annotation->getAnnotationPropertiesDomNode());
3529     d->m_undoStack->push(uc);
3530     d->m_prevPropsOfAnnotBeingModified.clear();
3531 }
3532 
3533 void Document::translatePageAnnotation(int page, Annotation *annotation, const NormalizedPoint &delta)
3534 {
3535     int complete = (annotation->flags() & Okular::Annotation::BeingMoved) == 0;
3536     QUndoCommand *uc = new Okular::TranslateAnnotationCommand(d, annotation, page, delta, complete);
3537     d->m_undoStack->push(uc);
3538 }
3539 
3540 void Document::adjustPageAnnotation(int page, Annotation *annotation, const Okular::NormalizedPoint &delta1, const Okular::NormalizedPoint &delta2)
3541 {
3542     const bool complete = (annotation->flags() & Okular::Annotation::BeingResized) == 0;
3543     QUndoCommand *uc = new Okular::AdjustAnnotationCommand(d, annotation, page, delta1, delta2, complete);
3544     d->m_undoStack->push(uc);
3545 }
3546 
3547 void Document::editPageAnnotationContents(int page, Annotation *annotation, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3548 {
3549     QString prevContents = annotation->contents();
3550     QUndoCommand *uc = new EditAnnotationContentsCommand(d, annotation, page, newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos);
3551     d->m_undoStack->push(uc);
3552 }
3553 
3554 bool Document::canRemovePageAnnotation(const Annotation *annotation) const
3555 {
3556     if (!annotation || (annotation->flags() & Annotation::DenyDelete)) {
3557         return false;
3558     }
3559 
3560     if ((annotation->flags() & Annotation::External) && !d->canRemoveExternalAnnotations()) {
3561         return false;
3562     }
3563 
3564     switch (annotation->subType()) {
3565     case Annotation::AText:
3566     case Annotation::ALine:
3567     case Annotation::AGeom:
3568     case Annotation::AHighlight:
3569     case Annotation::AStamp:
3570     case Annotation::AInk:
3571     case Annotation::ACaret:
3572         return true;
3573     default:
3574         return false;
3575     }
3576 }
3577 
3578 void Document::removePageAnnotation(int page, Annotation *annotation)
3579 {
3580     QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page);
3581     d->m_undoStack->push(uc);
3582 }
3583 
3584 void Document::removePageAnnotations(int page, const QList<Annotation *> &annotations)
3585 {
3586     d->m_undoStack->beginMacro(i18nc("remove a collection of annotations from the page", "remove annotations"));
3587     for (Annotation *annotation : annotations) {
3588         QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page);
3589         d->m_undoStack->push(uc);
3590     }
3591     d->m_undoStack->endMacro();
3592 }
3593 
3594 bool DocumentPrivate::canAddAnnotationsNatively() const
3595 {
3596     Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3597 
3598     if (iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Addition)) {
3599         return true;
3600     }
3601 
3602     return false;
3603 }
3604 
3605 bool DocumentPrivate::canModifyExternalAnnotations() const
3606 {
3607     Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3608 
3609     if (iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Modification)) {
3610         return true;
3611     }
3612 
3613     return false;
3614 }
3615 
3616 bool DocumentPrivate::canRemoveExternalAnnotations() const
3617 {
3618     Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3619 
3620     if (iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Removal)) {
3621         return true;
3622     }
3623 
3624     return false;
3625 }
3626 
3627 void Document::setPageTextSelection(int page, RegularAreaRect *rect, const QColor &color)
3628 {
3629     Page *kp = d->m_pagesVector[page];
3630     if (!d->m_generator || !kp) {
3631         return;
3632     }
3633 
3634     // add or remove the selection basing whether rect is null or not
3635     if (rect) {
3636         kp->d->setTextSelections(rect, color);
3637     } else {
3638         kp->d->deleteTextSelections();
3639     }
3640 
3641     // notify observers about the change
3642     foreachObserver(notifyPageChanged(page, DocumentObserver::TextSelection));
3643 }
3644 
3645 bool Document::canUndo() const
3646 {
3647     return d->m_undoStack->canUndo();
3648 }
3649 
3650 bool Document::canRedo() const
3651 {
3652     return d->m_undoStack->canRedo();
3653 }
3654 
3655 /* REFERENCE IMPLEMENTATION: better calling setViewport from other code
3656 void Document::setNextPage()
3657 {
3658     // advance page and set viewport on observers
3659     if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 )
3660         setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber + 1 ) );
3661 }
3662 
3663 void Document::setPrevPage()
3664 {
3665     // go to previous page and set viewport on observers
3666     if ( (*d->m_viewportIterator).pageNumber > 0 )
3667         setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber - 1 ) );
3668 }
3669 */
3670 
3671 void Document::setViewport(const DocumentViewport &viewport, DocumentObserver *excludeObserver, bool smoothMove, bool updateHistory)
3672 {
3673     if (!viewport.isValid()) {
3674         qCDebug(OkularCoreDebug) << "invalid viewport:" << viewport.toString();
3675         return;
3676     }
3677     if (viewport.pageNumber >= int(d->m_pagesVector.count())) {
3678         // qCDebug(OkularCoreDebug) << "viewport out of document:" << viewport.toString();
3679         return;
3680     }
3681 
3682     // if already broadcasted, don't redo it
3683     DocumentViewport &oldViewport = *d->m_viewportIterator;
3684     // disabled by enrico on 2005-03-18 (less debug output)
3685     // if ( viewport == oldViewport )
3686     //    qCDebug(OkularCoreDebug) << "setViewport with the same viewport.";
3687 
3688     const int oldPageNumber = oldViewport.pageNumber;
3689 
3690     // set internal viewport taking care of history
3691     if (oldViewport.pageNumber == viewport.pageNumber || !oldViewport.isValid() || !updateHistory) {
3692         // if page is unchanged save the viewport at current position in queue
3693         oldViewport = viewport;
3694     } else {
3695         // remove elements after viewportIterator in queue
3696         d->m_viewportHistory.erase(++d->m_viewportIterator, d->m_viewportHistory.end());
3697 
3698         // keep the list to a reasonable size by removing head when needed
3699         if (d->m_viewportHistory.size() >= OKULAR_HISTORY_MAXSTEPS) {
3700             d->m_viewportHistory.pop_front();
3701         }
3702 
3703         // add the item at the end of the queue
3704         d->m_viewportIterator = d->m_viewportHistory.insert(d->m_viewportHistory.end(), viewport);
3705     }
3706 
3707     const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3708 
3709     const bool currentPageChanged = (oldPageNumber != currentViewportPage);
3710 
3711     // notify change to all other (different from id) observers
3712     for (DocumentObserver *o : std::as_const(d->m_observers)) {
3713         if (o != excludeObserver) {
3714             o->notifyViewportChanged(smoothMove);
3715         }
3716 
3717         if (currentPageChanged) {
3718             o->notifyCurrentPageChanged(oldPageNumber, currentViewportPage);
3719         }
3720     }
3721 }
3722 
3723 void Document::setViewportPage(int page, DocumentObserver *excludeObserver, bool smoothMove)
3724 {
3725     // clamp page in range [0 ... numPages-1]
3726     if (page < 0) {
3727         page = 0;
3728     } else if (page > (int)d->m_pagesVector.count()) {
3729         page = d->m_pagesVector.count() - 1;
3730     }
3731 
3732     // make a viewport from the page and broadcast it
3733     setViewport(DocumentViewport(page), excludeObserver, smoothMove);
3734 }
3735 
3736 void Document::setZoom(int factor, DocumentObserver *excludeObserver)
3737 {
3738     // notify change to all other (different from id) observers
3739     for (DocumentObserver *o : std::as_const(d->m_observers)) {
3740         if (o != excludeObserver) {
3741             o->notifyZoom(factor);
3742         }
3743     }
3744 }
3745 
3746 void Document::setPrevViewport()
3747 // restore viewport from the history
3748 {
3749     if (d->m_viewportIterator != d->m_viewportHistory.begin()) {
3750         const int oldViewportPage = (*d->m_viewportIterator).pageNumber;
3751 
3752         // restore previous viewport and notify it to observers
3753         --d->m_viewportIterator;
3754         foreachObserver(notifyViewportChanged(true));
3755 
3756         const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3757         if (oldViewportPage != currentViewportPage)
3758             foreachObserver(notifyCurrentPageChanged(oldViewportPage, currentViewportPage));
3759     }
3760 }
3761 
3762 void Document::setNextViewport()
3763 // restore next viewport from the history
3764 {
3765     auto nextIterator = std::list<DocumentViewport>::const_iterator(d->m_viewportIterator);
3766     ++nextIterator;
3767     if (nextIterator != d->m_viewportHistory.end()) {
3768         const int oldViewportPage = (*d->m_viewportIterator).pageNumber;
3769 
3770         // restore next viewport and notify it to observers
3771         ++d->m_viewportIterator;
3772         foreachObserver(notifyViewportChanged(true));
3773 
3774         const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3775         if (oldViewportPage != currentViewportPage)
3776             foreachObserver(notifyCurrentPageChanged(oldViewportPage, currentViewportPage));
3777     }
3778 }
3779 
3780 void Document::setNextDocumentViewport(const DocumentViewport &viewport)
3781 {
3782     d->m_nextDocumentViewport = viewport;
3783 }
3784 
3785 void Document::setNextDocumentDestination(const QString &namedDestination)
3786 {
3787     d->m_nextDocumentDestination = namedDestination;
3788 }
3789 
3790 void Document::searchText(int searchID, const QString &text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor &color)
3791 {
3792     d->m_searchCancelled = false;
3793 
3794     // safety checks: don't perform searches on empty or unsearchable docs
3795     if (!d->m_generator || !d->m_generator->hasFeature(Generator::TextExtraction) || d->m_pagesVector.isEmpty()) {
3796         Q_EMIT searchFinished(searchID, NoMatchFound);
3797         return;
3798     }
3799 
3800     // if searchID search not recorded, create new descriptor and init params
3801     QMap<int, RunningSearch *>::iterator searchIt = d->m_searches.find(searchID);
3802     if (searchIt == d->m_searches.end()) {
3803         RunningSearch *search = new RunningSearch();
3804         search->continueOnPage = -1;
3805         searchIt = d->m_searches.insert(searchID, search);
3806     }
3807     RunningSearch *s = *searchIt;
3808 
3809     // update search structure
3810     bool newText = text != s->cachedString;
3811     s->cachedString = text;
3812     s->cachedType = type;
3813     s->cachedCaseSensitivity = caseSensitivity;
3814     s->cachedViewportMove = moveViewport;
3815     s->cachedColor = color;
3816     s->isCurrentlySearching = true;
3817 
3818     // global data for search
3819     QSet<int> *pagesToNotify = new QSet<int>;
3820 
3821     // remove highlights from pages and queue them for notifying changes
3822     *pagesToNotify += s->highlightedPages;
3823     for (const int pageNumber : std::as_const(s->highlightedPages)) {
3824         d->m_pagesVector.at(pageNumber)->d->deleteHighlights(searchID);
3825     }
3826     s->highlightedPages.clear();
3827 
3828     // set hourglass cursor
3829     QApplication::setOverrideCursor(Qt::WaitCursor);
3830 
3831     // 1. ALLDOC - process all document marking pages
3832     if (type == AllDocument) {
3833         QMap<Page *, QVector<RegularAreaRect *>> *pageMatches = new QMap<Page *, QVector<RegularAreaRect *>>;
3834 
3835         // search and highlight 'text' (as a solid phrase) on all pages
3836         QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID] { d->doContinueAllDocumentSearch(pagesToNotify, pageMatches, 0, searchID); });
3837     }
3838     // 2. NEXTMATCH - find next matching item (or start from top)
3839     // 3. PREVMATCH - find previous matching item (or start from bottom)
3840     else if (type == NextMatch || type == PreviousMatch) {
3841         // find out from where to start/resume search from
3842         const bool forward = type == NextMatch;
3843         const int viewportPage = (*d->m_viewportIterator).pageNumber;
3844         const int fromStartSearchPage = forward ? 0 : d->m_pagesVector.count() - 1;
3845         int currentPage = fromStart ? fromStartSearchPage : ((s->continueOnPage != -1) ? s->continueOnPage : viewportPage);
3846         Page *lastPage = fromStart ? nullptr : d->m_pagesVector[currentPage];
3847         int pagesDone = 0;
3848 
3849         // continue checking last TextPage first (if it is the current page)
3850         RegularAreaRect *match = nullptr;
3851         if (lastPage && lastPage->number() == s->continueOnPage) {
3852             if (newText) {
3853                 match = lastPage->findText(searchID, text, forward ? FromTop : FromBottom, caseSensitivity);
3854             } else {
3855                 match = lastPage->findText(searchID, text, forward ? NextResult : PreviousResult, caseSensitivity, &s->continueOnMatch);
3856             }
3857             if (!match) {
3858                 if (forward) {
3859                     currentPage++;
3860                 } else {
3861                     currentPage--;
3862                 }
3863                 pagesDone++;
3864             }
3865         }
3866 
3867         s->pagesDone = pagesDone;
3868 
3869         DoContinueDirectionMatchSearchStruct *searchStruct = new DoContinueDirectionMatchSearchStruct();
3870         searchStruct->pagesToNotify = pagesToNotify;
3871         searchStruct->match = match;
3872         searchStruct->currentPage = currentPage;
3873         searchStruct->searchID = searchID;
3874 
3875         QTimer::singleShot(0, this, [this, searchStruct] { d->doContinueDirectionMatchSearch(searchStruct); });
3876     }
3877     // 4. GOOGLE* - process all document marking pages
3878     else if (type == GoogleAll || type == GoogleAny) {
3879         QMap<Page *, QVector<QPair<RegularAreaRect *, QColor>>> *pageMatches = new QMap<Page *, QVector<QPair<RegularAreaRect *, QColor>>>;
3880         const QStringList words = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
3881 
3882         // search and highlight every word in 'text' on all pages
3883         QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID, words] { d->doContinueGooglesDocumentSearch(pagesToNotify, pageMatches, 0, searchID, words); });
3884     }
3885 }
3886 
3887 void Document::continueSearch(int searchID)
3888 {
3889     // check if searchID is present in runningSearches
3890     QMap<int, RunningSearch *>::const_iterator it = d->m_searches.constFind(searchID);
3891     if (it == d->m_searches.constEnd()) {
3892         Q_EMIT searchFinished(searchID, NoMatchFound);
3893         return;
3894     }
3895 
3896     // start search with cached parameters from last search by searchID
3897     RunningSearch *p = *it;
3898     if (!p->isCurrentlySearching) {
3899         searchText(searchID, p->cachedString, false, p->cachedCaseSensitivity, p->cachedType, p->cachedViewportMove, p->cachedColor);
3900     }
3901 }
3902 
3903 void Document::continueSearch(int searchID, SearchType type)
3904 {
3905     // check if searchID is present in runningSearches
3906     QMap<int, RunningSearch *>::const_iterator it = d->m_searches.constFind(searchID);
3907     if (it == d->m_searches.constEnd()) {
3908         Q_EMIT searchFinished(searchID, NoMatchFound);
3909         return;
3910     }
3911 
3912     // start search with cached parameters from last search by searchID
3913     RunningSearch *p = *it;
3914     if (!p->isCurrentlySearching) {
3915         searchText(searchID, p->cachedString, false, p->cachedCaseSensitivity, type, p->cachedViewportMove, p->cachedColor);
3916     }
3917 }
3918 
3919 void Document::resetSearch(int searchID)
3920 {
3921     // if we are closing down, don't bother doing anything
3922     if (!d->m_generator) {
3923         return;
3924     }
3925 
3926     // check if searchID is present in runningSearches
3927     QMap<int, RunningSearch *>::iterator searchIt = d->m_searches.find(searchID);
3928     if (searchIt == d->m_searches.end()) {
3929         return;
3930     }
3931 
3932     // get previous parameters for search
3933     RunningSearch *s = *searchIt;
3934 
3935     // unhighlight pages and inform observers about that
3936     for (const int pageNumber : std::as_const(s->highlightedPages)) {
3937         d->m_pagesVector.at(pageNumber)->d->deleteHighlights(searchID);
3938         foreachObserver(notifyPageChanged(pageNumber, DocumentObserver::Highlights));
3939     }
3940 
3941     // send the setup signal too (to update views that filter on matches)
3942     foreachObserver(notifySetup(d->m_pagesVector, 0));
3943 
3944     // remove search from the runningSearches list and delete it
3945     d->m_searches.erase(searchIt);
3946     delete s;
3947 }
3948 
3949 void Document::cancelSearch()
3950 {
3951     d->m_searchCancelled = true;
3952 }
3953 
3954 void Document::undo()
3955 {
3956     d->m_undoStack->undo();
3957 }
3958 
3959 void Document::redo()
3960 {
3961     d->m_undoStack->redo();
3962 }
3963 
3964 void Document::editFormText(int pageNumber, Okular::FormFieldText *form, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3965 {
3966     QUndoCommand *uc = new EditFormTextCommand(this->d, form, pageNumber, newContents, newCursorPos, form->text(), prevCursorPos, prevAnchorPos);
3967     d->m_undoStack->push(uc);
3968 }
3969 
3970 void Document::editFormList(int pageNumber, FormFieldChoice *form, const QList<int> &newChoices)
3971 {
3972     const QList<int> prevChoices = form->currentChoices();
3973     QUndoCommand *uc = new EditFormListCommand(this->d, form, pageNumber, newChoices, prevChoices);
3974     d->m_undoStack->push(uc);
3975 }
3976 
3977 void Document::editFormCombo(int pageNumber, FormFieldChoice *form, const QString &newText, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3978 {
3979     QString prevText;
3980     if (form->currentChoices().isEmpty()) {
3981         prevText = form->editChoice();
3982     } else {
3983         prevText = form->choices().at(form->currentChoices().constFirst());
3984     }
3985 
3986     QUndoCommand *uc = new EditFormComboCommand(this->d, form, pageNumber, newText, newCursorPos, prevText, prevCursorPos, prevAnchorPos);
3987     d->m_undoStack->push(uc);
3988 }
3989 
3990 void Document::editFormButtons(int pageNumber, const QList<FormFieldButton *> &formButtons, const QList<bool> &newButtonStates)
3991 {
3992     QUndoCommand *uc = new EditFormButtonsCommand(this->d, pageNumber, formButtons, newButtonStates);
3993     d->m_undoStack->push(uc);
3994 }
3995 
3996 void Document::reloadDocument() const
3997 {
3998     const int numOfPages = pages();
3999     for (int i = currentPage(); i >= 0; i--) {
4000         d->refreshPixmaps(i);
4001     }
4002     for (int i = currentPage() + 1; i < numOfPages; i++) {
4003         d->refreshPixmaps(i);
4004     }
4005 }
4006 
4007 BookmarkManager *Document::bookmarkManager() const
4008 {
4009     return d->m_bookmarkManager;
4010 }
4011 
4012 QList<int> Document::bookmarkedPageList() const
4013 {
4014     QList<int> list;
4015     uint docPages = pages();
4016 
4017     // pages are 0-indexed internally, but 1-indexed externally
4018     for (uint i = 0; i < docPages; i++) {
4019         if (bookmarkManager()->isBookmarked(i)) {
4020             list << i + 1;
4021         }
4022     }
4023     return list;
4024 }
4025 
4026 QString Document::bookmarkedPageRange() const
4027 {
4028     // Code formerly in Part::slotPrint()
4029     // range detecting
4030     QString range;
4031     uint docPages = pages();
4032     int startId = -1;
4033     int endId = -1;
4034 
4035     for (uint i = 0; i < docPages; ++i) {
4036         if (bookmarkManager()->isBookmarked(i)) {
4037             if (startId < 0) {
4038                 startId = i;
4039             }
4040             if (endId < 0) {
4041                 endId = startId;
4042             } else {
4043                 ++endId;
4044             }
4045         } else if (startId >= 0 && endId >= 0) {
4046             if (!range.isEmpty()) {
4047                 range += QLatin1Char(',');
4048             }
4049 
4050             if (endId - startId > 0) {
4051                 range += QStringLiteral("%1-%2").arg(startId + 1).arg(endId + 1);
4052             } else {
4053                 range += QString::number(startId + 1);
4054             }
4055             startId = -1;
4056             endId = -1;
4057         }
4058     }
4059     if (startId >= 0 && endId >= 0) {
4060         if (!range.isEmpty()) {
4061             range += QLatin1Char(',');
4062         }
4063 
4064         if (endId - startId > 0) {
4065             range += QStringLiteral("%1-%2").arg(startId + 1).arg(endId + 1);
4066         } else {
4067             range += QString::number(startId + 1);
4068         }
4069     }
4070     return range;
4071 }
4072 
4073 struct ExecuteNextActionsHelper : public QObject, private DocumentObserver {
4074     Q_OBJECT
4075 public:
4076     explicit ExecuteNextActionsHelper(Document *doc)
4077         : m_doc(doc)
4078     {
4079         doc->addObserver(this);
4080         connect(doc, &Document::aboutToClose, this, [this] { b = false; });
4081     }
4082 
4083     ~ExecuteNextActionsHelper() override
4084     {
4085         m_doc->removeObserver(this);
4086     }
4087 
4088     void notifySetup(const QVector<Okular::Page *> & /*pages*/, int setupFlags) override
4089     {
4090         if (setupFlags == DocumentChanged || setupFlags == UrlChanged) {
4091             b = false;
4092         }
4093     }
4094 
4095     bool shouldExecuteNextAction() const
4096     {
4097         return b;
4098     }
4099 
4100 private:
4101     Document *const m_doc;
4102     bool b = true;
4103 };
4104 
4105 void Document::processAction(const Action *action)
4106 {
4107     if (!action) {
4108         return;
4109     }
4110 
4111     // Don't execute next actions if the action itself caused the closing of the document
4112     const ExecuteNextActionsHelper executeNextActionsHelper(this);
4113 
4114     switch (action->actionType()) {
4115     case Action::Goto: {
4116         const GotoAction *go = static_cast<const GotoAction *>(action);
4117         d->m_nextDocumentViewport = go->destViewport();
4118         d->m_nextDocumentDestination = go->destinationName();
4119 
4120         // Explanation of why d->m_nextDocumentViewport is needed:
4121         // all openRelativeFile does is launch a signal telling we
4122         // want to open another URL, the problem is that when the file is
4123         // non local, the loading is done asynchronously so you can't
4124         // do a setViewport after the if as it was because you are doing the setViewport
4125         // on the old file and when the new arrives there is no setViewport for it and
4126         // it does not show anything
4127 
4128         // first open filename if link is pointing outside this document
4129         const QString filename = go->fileName();
4130         if (go->isExternal() && !d->openRelativeFile(filename)) {
4131             qCWarning(OkularCoreDebug).nospace() << "Action: Error opening '" << filename << "'.";
4132             break;
4133         } else {
4134             const DocumentViewport nextViewport = d->nextDocumentViewport();
4135             // skip local links that point to nowhere (broken ones)
4136             if (!nextViewport.isValid()) {
4137                 break;
4138             }
4139 
4140             setViewport(nextViewport, nullptr, true);
4141             d->m_nextDocumentViewport = DocumentViewport();
4142             d->m_nextDocumentDestination = QString();
4143         }
4144 
4145     } break;
4146 
4147     case Action::Execute: {
4148         const ExecuteAction *exe = static_cast<const ExecuteAction *>(action);
4149         const QString fileName = exe->fileName();
4150         if (fileName.endsWith(QLatin1String(".pdf"), Qt::CaseInsensitive)) {
4151             d->openRelativeFile(fileName);
4152             break;
4153         }
4154 
4155         // Albert: the only pdf i have that has that kind of link don't define
4156         // an application and use the fileName as the file to open
4157         QUrl url = d->giveAbsoluteUrl(fileName);
4158         QMimeDatabase db;
4159         QMimeType mime = db.mimeTypeForUrl(url);
4160         // Check executables
4161         if (KIO::OpenUrlJob::isExecutableFile(url, mime.name())) {
4162             // Don't have any pdf that uses this code path, just a guess on how it should work
4163             if (!exe->parameters().isEmpty()) {
4164                 url = d->giveAbsoluteUrl(exe->parameters());
4165                 mime = db.mimeTypeForUrl(url);
4166 
4167                 if (KIO::OpenUrlJob::isExecutableFile(url, mime.name())) {
4168                     // this case is a link pointing to an executable with a parameter
4169                     // that also is an executable, possibly a hand-crafted pdf
4170                     Q_EMIT error(i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that."), -1);
4171                     break;
4172                 }
4173             } else {
4174                 // this case is a link pointing to an executable with no parameters
4175                 // core developers find unacceptable executing it even after asking the user
4176                 Q_EMIT error(i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that."), -1);
4177                 break;
4178             }
4179         }
4180 
4181         KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mime.name());
4182         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->m_widget));
4183         job->start();
4184         connect(job, &KIO::OpenUrlJob::result, this, [this, mime](KJob *job) {
4185             if (job->error()) {
4186                 Q_EMIT error(i18n("No application found for opening file of mimetype %1.", mime.name()), -1);
4187             }
4188         });
4189     } break;
4190 
4191     case Action::DocAction: {
4192         const DocumentAction *docaction = static_cast<const DocumentAction *>(action);
4193         switch (docaction->documentActionType()) {
4194         case DocumentAction::PageFirst:
4195             setViewportPage(0);
4196             break;
4197         case DocumentAction::PagePrev:
4198             if ((*d->m_viewportIterator).pageNumber > 0) {
4199                 setViewportPage((*d->m_viewportIterator).pageNumber - 1);
4200             }
4201             break;
4202         case DocumentAction::PageNext:
4203             if ((*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1) {
4204                 setViewportPage((*d->m_viewportIterator).pageNumber + 1);
4205             }
4206             break;
4207         case DocumentAction::PageLast:
4208             setViewportPage(d->m_pagesVector.count() - 1);
4209             break;
4210         case DocumentAction::HistoryBack:
4211             setPrevViewport();
4212             break;
4213         case DocumentAction::HistoryForward:
4214             setNextViewport();
4215             break;
4216         case DocumentAction::Quit:
4217             Q_EMIT quit();
4218             break;
4219         case DocumentAction::Presentation:
4220             Q_EMIT linkPresentation();
4221             break;
4222         case DocumentAction::EndPresentation:
4223             Q_EMIT linkEndPresentation();
4224             break;
4225         case DocumentAction::Find:
4226             Q_EMIT linkFind();
4227             break;
4228         case DocumentAction::GoToPage:
4229             Q_EMIT linkGoToPage();
4230             break;
4231         case DocumentAction::Close:
4232             Q_EMIT close();
4233             break;
4234         case DocumentAction::Print:
4235             Q_EMIT requestPrint();
4236             break;
4237         case DocumentAction::SaveAs:
4238             Q_EMIT requestSaveAs();
4239             break;
4240         }
4241     } break;
4242 
4243     case Action::Browse: {
4244         const BrowseAction *browse = static_cast<const BrowseAction *>(action);
4245         QString lilySource;
4246         int lilyRow = 0, lilyCol = 0;
4247         // if the url is a mailto one, invoke mailer
4248         if (browse->url().scheme() == QLatin1String("mailto")) {
4249             QDesktopServices::openUrl(browse->url());
4250         } else if (extractLilyPondSourceReference(browse->url(), &lilySource, &lilyRow, &lilyCol)) {
4251             const SourceReference ref(lilySource, lilyRow, lilyCol);
4252             processSourceReference(&ref);
4253         } else {
4254             const QUrl url = browse->url();
4255 
4256             // fix for #100366, documents with relative links that are the form of http:foo.pdf
4257             if ((url.scheme() == QLatin1String("http")) && url.host().isEmpty() && url.fileName().endsWith(QLatin1String("pdf"))) {
4258                 d->openRelativeFile(url.fileName());
4259                 break;
4260             }
4261 
4262             // handle documents with relative path
4263             QUrl realUrl;
4264             if (d->m_url.isValid()) {
4265                 realUrl = KIO::upUrl(d->m_url).resolved(url);
4266             } else if (!url.isRelative()) {
4267                 realUrl = url;
4268             }
4269             if (realUrl.isValid()) {
4270                 auto *job = new KIO::OpenUrlJob(realUrl);
4271                 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->m_widget.data()));
4272                 job->start();
4273             }
4274         }
4275     } break;
4276 
4277     case Action::Sound: {
4278         const SoundAction *linksound = static_cast<const SoundAction *>(action);
4279         AudioPlayer::instance()->playSound(linksound->sound(), linksound);
4280     } break;
4281 
4282     case Action::Script: {
4283         const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4284         if (!d->m_scripter) {
4285             d->m_scripter = new Scripter(d);
4286         }
4287         d->m_scripter->execute(linkscript->scriptType(), linkscript->script());
4288     } break;
4289 
4290     case Action::Movie:
4291         Q_EMIT processMovieAction(static_cast<const MovieAction *>(action));
4292         break;
4293     case Action::Rendition: {
4294         const RenditionAction *linkrendition = static_cast<const RenditionAction *>(action);
4295         if (!linkrendition->script().isEmpty()) {
4296             if (!d->m_scripter) {
4297                 d->m_scripter = new Scripter(d);
4298             }
4299             d->m_scripter->execute(linkrendition->scriptType(), linkrendition->script());
4300         }
4301 
4302         Q_EMIT processRenditionAction(static_cast<const RenditionAction *>(action));
4303     } break;
4304     case Action::BackendOpaque: {
4305         d->m_generator->opaqueAction(static_cast<const BackendOpaqueAction *>(action));
4306     } break;
4307     }
4308 
4309     if (executeNextActionsHelper.shouldExecuteNextAction()) {
4310         const QVector<Action *> nextActions = action->nextActions();
4311         for (const Action *a : nextActions) {
4312             processAction(a);
4313         }
4314     }
4315 }
4316 
4317 void Document::processFormatAction(const Action *action, Okular::FormFieldText *fft)
4318 {
4319     if (action->actionType() != Action::Script) {
4320         qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for formatting.";
4321         return;
4322     }
4323 
4324     // Lookup the page of the FormFieldText
4325     int foundPage = d->findFieldPageNumber(fft);
4326 
4327     if (foundPage == -1) {
4328         qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4329         return;
4330     }
4331 
4332     const QString unformattedText = fft->text();
4333 
4334     std::shared_ptr<Event> event = Event::createFormatEvent(fft, d->m_pagesVector[foundPage]);
4335 
4336     const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4337 
4338     d->executeScriptEvent(event, linkscript);
4339 
4340     const QString formattedText = event->value().toString();
4341     if (formattedText != unformattedText) {
4342         // We set the formattedText, because when we call refreshFormWidget
4343         // It will set the QLineEdit to this formattedText
4344         fft->setText(formattedText);
4345         fft->setAppearanceText(formattedText);
4346         Q_EMIT refreshFormWidget(fft);
4347         d->refreshPixmaps(foundPage);
4348         // Then we make the form have the unformatted text, to use
4349         // in calculations and other things.
4350         fft->setText(unformattedText);
4351     } else if (fft->additionalAction(FormField::CalculateField)) {
4352         // When the field was calculated we need to refresh even
4353         // if the format script changed nothing. e.g. on error.
4354         // This is because the recalculateForms function delegated
4355         // the responsiblity for the refresh to us.
4356         Q_EMIT refreshFormWidget(fft);
4357         d->refreshPixmaps(foundPage);
4358     }
4359 }
4360 
4361 QString DocumentPrivate::diff(const QString &oldVal, const QString &newVal)
4362 {
4363     // We need to consider unicode surrogate pairs and others so working
4364     // with QString directly, even with the private QStringIterator is
4365     // not that simple to get right
4366     // so let's just convert to ucs4
4367     // also, given that toUcs4 is either a QList or a QVector depending on
4368     // qt version, let's try keep it very auto-typed to ease Qt6 porting
4369 
4370     auto oldUcs4 = oldVal.toStdU32String();
4371     auto newUcs4 = newVal.toStdU32String();
4372 
4373     for (size_t i = 0; i < std::min(oldUcs4.size(), newUcs4.size()); i++) {
4374         if (oldUcs4.at(i) != newUcs4.at(i)) {
4375             return QString::fromUcs4(std::u32string_view {newUcs4}.substr(i).data(), newUcs4.size() - i);
4376         }
4377     }
4378     if (oldUcs4.size() < newUcs4.size()) {
4379         return QString::fromUcs4(std::u32string_view {newUcs4}.substr(oldUcs4.size()).data(), newUcs4.size() - oldUcs4.size());
4380     }
4381     return {};
4382 }
4383 
4384 void Document::processKeystrokeAction(const Action *action, Okular::FormFieldText *fft, const QVariant &newValue)
4385 {
4386     if (action->actionType() != Action::Script) {
4387         qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for keystroke.";
4388         return;
4389     }
4390     // Lookup the page of the FormFieldText
4391     int foundPage = d->findFieldPageNumber(fft);
4392 
4393     if (foundPage == -1) {
4394         qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4395         return;
4396     }
4397 
4398     std::shared_ptr<Event> event = Event::createKeystrokeEvent(fft, d->m_pagesVector[foundPage]);
4399     event->setChange(DocumentPrivate::diff(fft->text(), newValue.toString()));
4400 
4401     const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4402 
4403     d->executeScriptEvent(event, linkscript);
4404 
4405     if (event->returnCode()) {
4406         fft->setText(newValue.toString());
4407     } else {
4408         Q_EMIT refreshFormWidget(fft);
4409     }
4410 }
4411 
4412 void Document::processKeystrokeCommitAction(const Action *action, Okular::FormFieldText *fft)
4413 {
4414     if (action->actionType() != Action::Script) {
4415         qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for keystroke.";
4416         return;
4417     }
4418     // Lookup the page of the FormFieldText
4419     int foundPage = d->findFieldPageNumber(fft);
4420 
4421     if (foundPage == -1) {
4422         qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4423         return;
4424     }
4425 
4426     std::shared_ptr<Event> event = Event::createKeystrokeEvent(fft, d->m_pagesVector[foundPage]);
4427     event->setWillCommit(true);
4428 
4429     const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4430 
4431     d->executeScriptEvent(event, linkscript);
4432 
4433     if (event->returnCode()) {
4434         fft->setText(event->value().toString());
4435         // TODO commit value
4436     } else {
4437         // TODO reset to committed value
4438     }
4439 }
4440 
4441 void Document::processFocusAction(const Action *action, Okular::FormField *field)
4442 {
4443     if (!action || action->actionType() != Action::Script) {
4444         return;
4445     }
4446 
4447     // Lookup the page of the FormFieldText
4448     int foundPage = d->findFieldPageNumber(field);
4449 
4450     if (foundPage == -1) {
4451         qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4452         return;
4453     }
4454 
4455     std::shared_ptr<Event> event = Event::createFormFocusEvent(field, d->m_pagesVector[foundPage]);
4456 
4457     const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4458 
4459     d->executeScriptEvent(event, linkscript);
4460 }
4461 
4462 void Document::processValidateAction(const Action *action, Okular::FormFieldText *fft, bool &returnCode)
4463 {
4464     if (!action || action->actionType() != Action::Script) {
4465         return;
4466     }
4467 
4468     // Lookup the page of the FormFieldText
4469     int foundPage = d->findFieldPageNumber(fft);
4470 
4471     if (foundPage == -1) {
4472         qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4473         return;
4474     }
4475 
4476     std::shared_ptr<Event> event = Event::createFormValidateEvent(fft, d->m_pagesVector[foundPage]);
4477 
4478     const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4479 
4480     d->executeScriptEvent(event, linkscript);
4481     returnCode = event->returnCode();
4482 }
4483 
4484 void Document::processFormMouseUpScripAction(const Action *action, Okular::FormField *ff)
4485 {
4486     if (!action || action->actionType() != Action::Script) {
4487         return;
4488     }
4489 
4490     // Lookup the page of the FormFieldText
4491     int foundPage = d->findFieldPageNumber(ff);
4492 
4493     if (foundPage == -1) {
4494         qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4495         return;
4496     }
4497 
4498     std::shared_ptr<Event> event = Event::createFieldMouseUpEvent(ff, d->m_pagesVector[foundPage]);
4499 
4500     const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4501 
4502     d->executeScriptEvent(event, linkscript);
4503 }
4504 
4505 void Document::processSourceReference(const SourceReference *ref)
4506 {
4507     if (!ref) {
4508         return;
4509     }
4510 
4511     const QUrl url = d->giveAbsoluteUrl(ref->fileName());
4512     if (!url.isLocalFile()) {
4513         qCDebug(OkularCoreDebug) << url.url() << "is not a local file.";
4514         return;
4515     }
4516 
4517     const QString absFileName = url.toLocalFile();
4518     if (!QFile::exists(absFileName)) {
4519         qCDebug(OkularCoreDebug) << "No such file:" << absFileName;
4520         return;
4521     }
4522 
4523     bool handled = false;
4524     Q_EMIT sourceReferenceActivated(absFileName, ref->row(), ref->column(), &handled);
4525     if (handled) {
4526         return;
4527     }
4528 
4529     static QHash<int, QString> editors;
4530     // init the editors table if empty (on first run, usually)
4531     if (editors.isEmpty()) {
4532         editors = buildEditorsMap();
4533     }
4534 
4535     // prefer the editor from the command line
4536     QString p = d->editorCommandOverride;
4537     if (p.isEmpty()) {
4538         QHash<int, QString>::const_iterator it = editors.constFind(SettingsCore::externalEditor());
4539         if (it != editors.constEnd()) {
4540             p = *it;
4541         } else {
4542             p = SettingsCore::externalEditorCommand();
4543         }
4544     }
4545     // custom editor not yet configured
4546     if (p.isEmpty()) {
4547         return;
4548     }
4549 
4550     // manually append the %f placeholder if not specified
4551     if (p.indexOf(QLatin1String("%f")) == -1) {
4552         p.append(QLatin1String(" %f"));
4553     }
4554 
4555     // replacing the placeholders
4556     QHash<QChar, QString> map;
4557     map.insert(QLatin1Char('f'), absFileName);
4558     map.insert(QLatin1Char('c'), QString::number(ref->column()));
4559     map.insert(QLatin1Char('l'), QString::number(ref->row()));
4560     const QString cmd = KMacroExpander::expandMacrosShellQuote(p, map);
4561     if (cmd.isEmpty()) {
4562         return;
4563     }
4564     QStringList args = KShell::splitArgs(cmd);
4565     if (args.isEmpty()) {
4566         return;
4567     }
4568 
4569     const QString prog = args.takeFirst();
4570     // Make sure prog is in PATH and not just in the CWD
4571     const QString progFullPath = QStandardPaths::findExecutable(prog);
4572     if (progFullPath.isEmpty()) {
4573         return;
4574     }
4575 
4576     KProcess::startDetached(progFullPath, args);
4577 }
4578 
4579 const SourceReference *Document::dynamicSourceReference(int pageNr, double absX, double absY)
4580 {
4581     if (!d->m_synctex_scanner) {
4582         return nullptr;
4583     }
4584 
4585     const QSizeF dpi = d->m_generator->dpi();
4586 
4587     if (synctex_edit_query(d->m_synctex_scanner, pageNr + 1, absX * 72. / dpi.width(), absY * 72. / dpi.height()) > 0) {
4588         synctex_node_p node;
4589         // TODO what should we do if there is really more than one node?
4590         while ((node = synctex_scanner_next_result(d->m_synctex_scanner))) {
4591             int line = synctex_node_line(node);
4592             int col = synctex_node_column(node);
4593             // column extraction does not seem to be implemented in synctex so far. set the SourceReference default value.
4594             if (col == -1) {
4595                 col = 0;
4596             }
4597             const char *name = synctex_scanner_get_name(d->m_synctex_scanner, synctex_node_tag(node));
4598 
4599             return new Okular::SourceReference(QFile::decodeName(name), line, col);
4600         }
4601     }
4602     return nullptr;
4603 }
4604 
4605 Document::PrintingType Document::printingSupport() const
4606 {
4607     if (d->m_generator) {
4608         if (d->m_generator->hasFeature(Generator::PrintNative)) {
4609             return NativePrinting;
4610         }
4611 
4612 #ifndef Q_OS_WIN
4613         if (d->m_generator->hasFeature(Generator::PrintPostscript)) {
4614             return PostscriptPrinting;
4615         }
4616 #endif
4617     }
4618 
4619     return NoPrinting;
4620 }
4621 
4622 bool Document::supportsPrintToFile() const
4623 {
4624     return d->m_generator ? d->m_generator->hasFeature(Generator::PrintToFile) : false;
4625 }
4626 
4627 Document::PrintError Document::print(QPrinter &printer)
4628 {
4629     return d->m_generator ? d->m_generator->print(printer) : Document::UnknownPrintError;
4630 }
4631 
4632 QString Document::printErrorString(PrintError error)
4633 {
4634     switch (error) {
4635     case TemporaryFileOpenPrintError:
4636         return i18n("Could not open a temporary file");
4637     case FileConversionPrintError:
4638         return i18n("Print conversion failed");
4639     case PrintingProcessCrashPrintError:
4640         return i18n("Printing process crashed");
4641     case PrintingProcessStartPrintError:
4642         return i18n("Printing process could not start");
4643     case PrintToFilePrintError:
4644         return i18n("Printing to file failed");
4645     case InvalidPrinterStatePrintError:
4646         return i18n("Printer was in invalid state");
4647     case UnableToFindFilePrintError:
4648         return i18n("Unable to find file to print");
4649     case NoFileToPrintError:
4650         return i18n("There was no file to print");
4651     case NoBinaryToPrintError:
4652         return i18n("Could not find a suitable binary for printing. Make sure CUPS lpr binary is available");
4653     case InvalidPageSizePrintError:
4654         return i18n("The page print size is invalid");
4655     case NoPrintError:
4656         return QString();
4657     case UnknownPrintError:
4658         return QString();
4659     }
4660 
4661     return QString();
4662 }
4663 
4664 QWidget *Document::printConfigurationWidget() const
4665 {
4666     if (d->m_generator) {
4667         PrintInterface *iface = qobject_cast<Okular::PrintInterface *>(d->m_generator);
4668         return iface ? iface->printConfigurationWidget() : nullptr;
4669     } else {
4670         return nullptr;
4671     }
4672 }
4673 
4674 void Document::fillConfigDialog(KConfigDialog *dialog)
4675 {
4676     if (!dialog) {
4677         return;
4678     }
4679 
4680     // We know it's a BackendConfigDialog, but check anyway
4681     BackendConfigDialog *bcd = dynamic_cast<BackendConfigDialog *>(dialog);
4682     if (!bcd) {
4683         return;
4684     }
4685 
4686     // ensure that we have all the generators with settings loaded
4687     QVector<KPluginMetaData> offers = DocumentPrivate::configurableGenerators();
4688     d->loadServiceList(offers);
4689 
4690     // We want the generators to be sorted by name so let's fill in a QMap
4691     // this sorts by internal id which is not awesome, but at least the sorting
4692     // is stable between runs that before it wasn't
4693     QMap<QString, GeneratorInfo> sortedGenerators;
4694     QHash<QString, GeneratorInfo>::iterator it = d->m_loadedGenerators.begin();
4695     QHash<QString, GeneratorInfo>::iterator itEnd = d->m_loadedGenerators.end();
4696     for (; it != itEnd; ++it) {
4697         sortedGenerators.insert(it.key(), it.value());
4698     }
4699 
4700     bool pagesAdded = false;
4701     QMap<QString, GeneratorInfo>::iterator sit = sortedGenerators.begin();
4702     QMap<QString, GeneratorInfo>::iterator sitEnd = sortedGenerators.end();
4703     for (; sit != sitEnd; ++sit) {
4704         Okular::ConfigInterface *iface = d->generatorConfig(sit.value());
4705         if (iface) {
4706             iface->addPages(dialog);
4707             pagesAdded = true;
4708 
4709             if (sit.value().generator == d->m_generator) {
4710                 const int rowCount = bcd->thePageWidget()->model()->rowCount();
4711                 KPageView *view = bcd->thePageWidget();
4712                 view->setCurrentPage(view->model()->index(rowCount - 1, 0));
4713             }
4714         }
4715     }
4716     if (pagesAdded) {
4717         connect(dialog, &KConfigDialog::settingsChanged, this, [this] { d->slotGeneratorConfigChanged(); });
4718     }
4719 }
4720 
4721 QVector<KPluginMetaData> DocumentPrivate::configurableGenerators()
4722 {
4723     const QVector<KPluginMetaData> available = availableGenerators();
4724     QVector<KPluginMetaData> result;
4725     for (const KPluginMetaData &md : available) {
4726         if (md.rawData()[QStringLiteral("X-KDE-okularHasInternalSettings")].toBool()) {
4727             result << md;
4728         }
4729     }
4730     return result;
4731 }
4732 
4733 KPluginMetaData Document::generatorInfo() const
4734 {
4735     if (!d->m_generator) {
4736         return KPluginMetaData();
4737     }
4738 
4739     auto genIt = d->m_loadedGenerators.constFind(d->m_generatorName);
4740     Q_ASSERT(genIt != d->m_loadedGenerators.constEnd());
4741     return genIt.value().metadata;
4742 }
4743 
4744 int Document::configurableGenerators() const
4745 {
4746     return DocumentPrivate::configurableGenerators().size();
4747 }
4748 
4749 QStringList Document::supportedMimeTypes() const
4750 {
4751     // TODO: make it a static member of DocumentPrivate?
4752     QStringList result = d->m_supportedMimeTypes;
4753     if (result.isEmpty()) {
4754         const QVector<KPluginMetaData> available = DocumentPrivate::availableGenerators();
4755         for (const KPluginMetaData &md : available) {
4756             result << md.mimeTypes();
4757         }
4758 
4759         // Remove duplicate mimetypes represented by different names
4760         QMimeDatabase mimeDatabase;
4761         QSet<QMimeType> uniqueMimetypes;
4762         for (const QString &mimeName : std::as_const(result)) {
4763             uniqueMimetypes.insert(mimeDatabase.mimeTypeForName(mimeName));
4764         }
4765         result.clear();
4766         for (const QMimeType &mimeType : uniqueMimetypes) {
4767             result.append(mimeType.name());
4768         }
4769 
4770         // Add the Okular archive mimetype
4771         result << QStringLiteral("application/vnd.kde.okular-archive");
4772 
4773         // Sorting by mimetype name doesn't make a ton of sense,
4774         // but ensures that the list is ordered the same way every time
4775         std::sort(result.begin(), result.end());
4776 
4777         d->m_supportedMimeTypes = result;
4778     }
4779     return result;
4780 }
4781 
4782 bool Document::canSwapBackingFile() const
4783 {
4784     if (!d->m_generator) {
4785         return false;
4786     }
4787 
4788     return d->m_generator->hasFeature(Generator::SwapBackingFile);
4789 }
4790 
4791 bool Document::swapBackingFile(const QString &newFileName, const QUrl &url)
4792 {
4793     if (!d->m_generator) {
4794         return false;
4795     }
4796 
4797     if (!d->m_generator->hasFeature(Generator::SwapBackingFile)) {
4798         return false;
4799     }
4800 
4801     // Save metadata about the file we're about to close
4802     d->saveDocumentInfo();
4803 
4804     d->clearAndWaitForRequests();
4805 
4806     qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName;
4807     QVector<Page *> newPagesVector;
4808     Generator::SwapBackingFileResult result = d->m_generator->swapBackingFile(newFileName, newPagesVector);
4809     if (result != Generator::SwapBackingFileError) {
4810         QList<ObjectRect *> rectsToDelete;
4811         QList<Annotation *> annotationsToDelete;
4812         QSet<PagePrivate *> pagePrivatesToDelete;
4813 
4814         if (result == Generator::SwapBackingFileReloadInternalData) {
4815             // Here we need to replace everything that the old generator
4816             // had created with what the new one has without making it look like
4817             // we have actually closed and opened the file again
4818 
4819             // Simple sanity check
4820             if (newPagesVector.count() != d->m_pagesVector.count()) {
4821                 return false;
4822             }
4823 
4824             // Update the undo stack contents
4825             for (int i = 0; i < d->m_undoStack->count(); ++i) {
4826                 // Trust me on the const_cast ^_^
4827                 QUndoCommand *uc = const_cast<QUndoCommand *>(d->m_undoStack->command(i));
4828                 if (OkularUndoCommand *ouc = dynamic_cast<OkularUndoCommand *>(uc)) {
4829                     const bool success = ouc->refreshInternalPageReferences(newPagesVector);
4830                     if (!success) {
4831                         qWarning() << "Document::swapBackingFile: refreshInternalPageReferences failed" << ouc;
4832                         return false;
4833                     }
4834                 } else {
4835                     qWarning() << "Document::swapBackingFile: Unhandled undo command" << uc;
4836                     return false;
4837                 }
4838             }
4839 
4840             for (int i = 0; i < d->m_pagesVector.count(); ++i) {
4841                 // switch the PagePrivate* from newPage to oldPage
4842                 // this way everyone still holding Page* doesn't get
4843                 // disturbed by it
4844                 Page *oldPage = d->m_pagesVector[i];
4845                 Page *newPage = newPagesVector[i];
4846                 newPage->d->adoptGeneratedContents(oldPage->d);
4847 
4848                 pagePrivatesToDelete << oldPage->d;
4849                 oldPage->d = newPage->d;
4850                 oldPage->d->m_page = oldPage;
4851                 oldPage->d->m_doc = d;
4852                 newPage->d = nullptr;
4853 
4854                 annotationsToDelete << oldPage->m_annotations;
4855                 rectsToDelete << oldPage->m_rects;
4856                 oldPage->m_annotations = newPage->m_annotations;
4857                 oldPage->m_rects = newPage->m_rects;
4858             }
4859             qDeleteAll(newPagesVector);
4860         }
4861 
4862         d->m_url = url;
4863         d->m_docFileName = newFileName;
4864         d->updateMetadataXmlNameAndDocSize();
4865         d->m_bookmarkManager->setUrl(d->m_url);
4866         d->m_documentInfo = DocumentInfo();
4867         d->m_documentInfoAskedKeys.clear();
4868 
4869         if (d->m_synctex_scanner) {
4870             synctex_scanner_free(d->m_synctex_scanner);
4871             d->m_synctex_scanner = synctex_scanner_new_with_output_file(QFile::encodeName(newFileName).constData(), nullptr, 1);
4872             if (!d->m_synctex_scanner && QFile::exists(newFileName + QLatin1String("sync"))) {
4873                 d->loadSyncFile(newFileName);
4874             }
4875         }
4876 
4877         foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::UrlChanged));
4878 
4879         qDeleteAll(annotationsToDelete);
4880         qDeleteAll(rectsToDelete);
4881         qDeleteAll(pagePrivatesToDelete);
4882 
4883         return true;
4884     } else {
4885         return false;
4886     }
4887 }
4888 
4889 bool Document::swapBackingFileArchive(const QString &newFileName, const QUrl &url)
4890 {
4891     qCDebug(OkularCoreDebug) << "Swapping backing archive to" << newFileName;
4892 
4893     ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive(newFileName);
4894     if (!newArchive) {
4895         return false;
4896     }
4897 
4898     const QString tempFileName = newArchive->document.fileName();
4899 
4900     const bool success = swapBackingFile(tempFileName, url);
4901 
4902     if (success) {
4903         delete d->m_archiveData;
4904         d->m_archiveData = newArchive;
4905     }
4906 
4907     return success;
4908 }
4909 
4910 void Document::setHistoryClean(bool clean)
4911 {
4912     if (clean) {
4913         d->m_undoStack->setClean();
4914     } else {
4915         d->m_undoStack->resetClean();
4916     }
4917 }
4918 
4919 bool Document::isHistoryClean() const
4920 {
4921     return d->m_undoStack->isClean();
4922 }
4923 
4924 bool Document::canSaveChanges() const
4925 {
4926     if (!d->m_generator) {
4927         return false;
4928     }
4929     Q_ASSERT(!d->m_generatorName.isEmpty());
4930 
4931     QHash<QString, GeneratorInfo>::iterator genIt = d->m_loadedGenerators.find(d->m_generatorName);
4932     Q_ASSERT(genIt != d->m_loadedGenerators.end());
4933     SaveInterface *saveIface = d->generatorSave(genIt.value());
4934     if (!saveIface) {
4935         return false;
4936     }
4937 
4938     return saveIface->supportsOption(SaveInterface::SaveChanges);
4939 }
4940 
4941 bool Document::canSaveChanges(SaveCapability cap) const
4942 {
4943     switch (cap) {
4944     case SaveFormsCapability:
4945         /* Assume that if the generator supports saving, forms can be saved.
4946          * We have no means to actually query the generator at the moment
4947          * TODO: Add some method to query the generator in SaveInterface */
4948         return canSaveChanges();
4949 
4950     case SaveAnnotationsCapability:
4951         return d->canAddAnnotationsNatively();
4952     }
4953 
4954     return false;
4955 }
4956 
4957 bool Document::saveChanges(const QString &fileName)
4958 {
4959     QString errorText;
4960     return saveChanges(fileName, &errorText);
4961 }
4962 
4963 bool Document::saveChanges(const QString &fileName, QString *errorText)
4964 {
4965     if (!d->m_generator || fileName.isEmpty()) {
4966         return false;
4967     }
4968     Q_ASSERT(!d->m_generatorName.isEmpty());
4969 
4970     QHash<QString, GeneratorInfo>::iterator genIt = d->m_loadedGenerators.find(d->m_generatorName);
4971     Q_ASSERT(genIt != d->m_loadedGenerators.end());
4972     SaveInterface *saveIface = d->generatorSave(genIt.value());
4973     if (!saveIface || !saveIface->supportsOption(SaveInterface::SaveChanges)) {
4974         return false;
4975     }
4976 
4977     return saveIface->save(fileName, SaveInterface::SaveChanges, errorText);
4978 }
4979 
4980 void Document::registerView(View *view)
4981 {
4982     if (!view) {
4983         return;
4984     }
4985 
4986     Document *viewDoc = view->viewDocument();
4987     if (viewDoc) {
4988         // check if already registered for this document
4989         if (viewDoc == this) {
4990             return;
4991         }
4992 
4993         viewDoc->unregisterView(view);
4994     }
4995 
4996     d->m_views.insert(view);
4997     view->d_func()->document = d;
4998 }
4999 
5000 void Document::unregisterView(View *view)
5001 {
5002     if (!view) {
5003         return;
5004     }
5005 
5006     Document *viewDoc = view->viewDocument();
5007     if (!viewDoc || viewDoc != this) {
5008         return;
5009     }
5010 
5011     view->d_func()->document = nullptr;
5012     d->m_views.remove(view);
5013 }
5014 
5015 QByteArray Document::fontData(const FontInfo &font) const
5016 {
5017     if (d->m_generator) {
5018         return d->m_generator->requestFontData(font);
5019     }
5020 
5021     return {};
5022 }
5023 
5024 ArchiveData *DocumentPrivate::unpackDocumentArchive(const QString &archivePath)
5025 {
5026     QMimeDatabase db;
5027     const QMimeType mime = db.mimeTypeForFile(archivePath, QMimeDatabase::MatchExtension);
5028     if (!mime.inherits(QStringLiteral("application/vnd.kde.okular-archive"))) {
5029         return nullptr;
5030     }
5031 
5032     KZip okularArchive(archivePath);
5033     if (!okularArchive.open(QIODevice::ReadOnly)) {
5034         return nullptr;
5035     }
5036 
5037     const KArchiveDirectory *mainDir = okularArchive.directory();
5038 
5039     // Check the archive doesn't have folders, we don't create them when saving the archive
5040     // and folders mean paths and paths mean path traversal issues
5041     const QStringList mainDirEntries = mainDir->entries();
5042     for (const QString &entry : mainDirEntries) {
5043         if (mainDir->entry(entry)->isDirectory()) {
5044             qWarning() << "Warning: Found a directory inside" << archivePath << " - Okular does not create files like that so it is most probably forged.";
5045             return nullptr;
5046         }
5047     }
5048 
5049     const KArchiveEntry *mainEntry = mainDir->entry(QStringLiteral("content.xml"));
5050     if (!mainEntry || !mainEntry->isFile()) {
5051         return nullptr;
5052     }
5053 
5054     std::unique_ptr<QIODevice> mainEntryDevice(static_cast<const KZipFileEntry *>(mainEntry)->createDevice());
5055     QDomDocument doc;
5056     if (!doc.setContent(mainEntryDevice.get())) {
5057         return nullptr;
5058     }
5059     mainEntryDevice.reset();
5060 
5061     QDomElement root = doc.documentElement();
5062     if (root.tagName() != QLatin1String("OkularArchive")) {
5063         return nullptr;
5064     }
5065 
5066     QString documentFileName;
5067     QString metadataFileName;
5068     QDomElement el = root.firstChild().toElement();
5069     for (; !el.isNull(); el = el.nextSibling().toElement()) {
5070         if (el.tagName() == QLatin1String("Files")) {
5071             QDomElement fileEl = el.firstChild().toElement();
5072             for (; !fileEl.isNull(); fileEl = fileEl.nextSibling().toElement()) {
5073                 if (fileEl.tagName() == QLatin1String("DocumentFileName")) {
5074                     documentFileName = fileEl.text();
5075                 } else if (fileEl.tagName() == QLatin1String("MetadataFileName")) {
5076                     metadataFileName = fileEl.text();
5077                 }
5078             }
5079         }
5080     }
5081     if (documentFileName.isEmpty()) {
5082         return nullptr;
5083     }
5084 
5085     const KArchiveEntry *docEntry = mainDir->entry(documentFileName);
5086     if (!docEntry || !docEntry->isFile()) {
5087         return nullptr;
5088     }
5089 
5090     std::unique_ptr<ArchiveData> archiveData(new ArchiveData());
5091     const int dotPos = documentFileName.indexOf(QLatin1Char('.'));
5092     if (dotPos != -1) {
5093         archiveData->document.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX") + documentFileName.mid(dotPos));
5094     }
5095     if (!archiveData->document.open()) {
5096         return nullptr;
5097     }
5098 
5099     archiveData->originalFileName = documentFileName;
5100 
5101     {
5102         std::unique_ptr<QIODevice> docEntryDevice(static_cast<const KZipFileEntry *>(docEntry)->createDevice());
5103         copyQIODevice(docEntryDevice.get(), &archiveData->document);
5104         archiveData->document.close();
5105     }
5106 
5107     const KArchiveEntry *metadataEntry = mainDir->entry(metadataFileName);
5108     if (metadataEntry && metadataEntry->isFile()) {
5109         std::unique_ptr<QIODevice> metadataEntryDevice(static_cast<const KZipFileEntry *>(metadataEntry)->createDevice());
5110         archiveData->metadataFile.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX.xml"));
5111         if (archiveData->metadataFile.open()) {
5112             copyQIODevice(metadataEntryDevice.get(), &archiveData->metadataFile);
5113             archiveData->metadataFile.close();
5114         }
5115     }
5116 
5117     return archiveData.release();
5118 }
5119 
5120 Document::OpenResult Document::openDocumentArchive(const QString &docFile, const QUrl &url, const QString &password)
5121 {
5122     d->m_archiveData = DocumentPrivate::unpackDocumentArchive(docFile);
5123     if (!d->m_archiveData) {
5124         return OpenError;
5125     }
5126 
5127     const QString tempFileName = d->m_archiveData->document.fileName();
5128     QMimeDatabase db;
5129     const QMimeType docMime = db.mimeTypeForFile(tempFileName, QMimeDatabase::MatchExtension);
5130     const OpenResult ret = openDocument(tempFileName, url, docMime, password);
5131 
5132     if (ret != OpenSuccess) {
5133         delete d->m_archiveData;
5134         d->m_archiveData = nullptr;
5135     }
5136 
5137     return ret;
5138 }
5139 
5140 bool Document::saveDocumentArchive(const QString &fileName)
5141 {
5142     if (!d->m_generator) {
5143         return false;
5144     }
5145 
5146     /* If we opened an archive, use the name of original file (eg foo.pdf)
5147      * instead of the archive's one (eg foo.okular) */
5148     QString docFileName = d->m_archiveData ? d->m_archiveData->originalFileName : d->m_url.fileName();
5149     if (docFileName == QLatin1String("-")) {
5150         return false;
5151     }
5152 
5153     QString docPath = d->m_docFileName;
5154     const QFileInfo fi(docPath);
5155     if (fi.isSymLink()) {
5156         docPath = fi.symLinkTarget();
5157     }
5158 
5159     KZip okularArchive(fileName);
5160     if (!okularArchive.open(QIODevice::WriteOnly)) {
5161         return false;
5162     }
5163 
5164     const KUser user;
5165 #ifndef Q_OS_WIN
5166     const KUserGroup userGroup(user.groupId());
5167 #else
5168     const KUserGroup userGroup(QStringLiteral(""));
5169 #endif
5170 
5171     QDomDocument contentDoc(QStringLiteral("OkularArchive"));
5172     QDomProcessingInstruction xmlPi = contentDoc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
5173     contentDoc.appendChild(xmlPi);
5174     QDomElement root = contentDoc.createElement(QStringLiteral("OkularArchive"));
5175     contentDoc.appendChild(root);
5176 
5177     QDomElement filesNode = contentDoc.createElement(QStringLiteral("Files"));
5178     root.appendChild(filesNode);
5179 
5180     QDomElement fileNameNode = contentDoc.createElement(QStringLiteral("DocumentFileName"));
5181     filesNode.appendChild(fileNameNode);
5182     fileNameNode.appendChild(contentDoc.createTextNode(docFileName));
5183 
5184     QDomElement metadataFileNameNode = contentDoc.createElement(QStringLiteral("MetadataFileName"));
5185     filesNode.appendChild(metadataFileNameNode);
5186     metadataFileNameNode.appendChild(contentDoc.createTextNode(QStringLiteral("metadata.xml")));
5187 
5188     // If the generator can save annotations natively, do it
5189     QTemporaryFile modifiedFile;
5190     bool annotationsSavedNatively = false;
5191     bool formsSavedNatively = false;
5192     if (d->canAddAnnotationsNatively() || canSaveChanges(SaveFormsCapability)) {
5193         if (!modifiedFile.open()) {
5194             return false;
5195         }
5196 
5197         const QString modifiedFileName = modifiedFile.fileName();
5198 
5199         modifiedFile.close(); // We're only interested in the file name
5200 
5201         QString errorText;
5202         if (saveChanges(modifiedFileName, &errorText)) {
5203             docPath = modifiedFileName; // Save this instead of the original file
5204             annotationsSavedNatively = d->canAddAnnotationsNatively();
5205             formsSavedNatively = canSaveChanges(SaveFormsCapability);
5206         } else {
5207             qCWarning(OkularCoreDebug) << "saveChanges failed: " << errorText;
5208             qCDebug(OkularCoreDebug) << "Falling back to saving a copy of the original file";
5209         }
5210     }
5211 
5212     PageItems saveWhat = None;
5213     if (!annotationsSavedNatively) {
5214         saveWhat |= AnnotationPageItems;
5215     }
5216     if (!formsSavedNatively) {
5217         saveWhat |= FormFieldPageItems;
5218     }
5219 
5220     QTemporaryFile metadataFile;
5221     if (!d->savePageDocumentInfo(&metadataFile, saveWhat)) {
5222         return false;
5223     }
5224 
5225     const QByteArray contentDocXml = contentDoc.toByteArray();
5226     const mode_t perm = 0100644;
5227     okularArchive.writeFile(QStringLiteral("content.xml"), contentDocXml, perm, user.loginName(), userGroup.name());
5228 
5229     okularArchive.addLocalFile(docPath, docFileName);
5230     okularArchive.addLocalFile(metadataFile.fileName(), QStringLiteral("metadata.xml"));
5231 
5232     if (!okularArchive.close()) {
5233         return false;
5234     }
5235 
5236     return true;
5237 }
5238 
5239 bool Document::extractArchivedFile(const QString &destFileName)
5240 {
5241     if (!d->m_archiveData) {
5242         return false;
5243     }
5244 
5245     // Remove existing file, if present (QFile::copy doesn't overwrite by itself)
5246     QFile::remove(destFileName);
5247 
5248     return d->m_archiveData->document.copy(destFileName);
5249 }
5250 
5251 QPageLayout::Orientation Document::orientation() const
5252 {
5253     double width, height;
5254     int landscape, portrait;
5255     const Okular::Page *currentPage;
5256 
5257     // if some pages are landscape and others are not, the most common wins, as
5258     // QPrinter does not accept a per-page setting
5259     landscape = 0;
5260     portrait = 0;
5261     for (uint i = 0; i < pages(); i++) {
5262         currentPage = page(i);
5263         width = currentPage->width();
5264         height = currentPage->height();
5265         if (currentPage->orientation() == Okular::Rotation90 || currentPage->orientation() == Okular::Rotation270) {
5266             std::swap(width, height);
5267         }
5268         if (width > height) {
5269             landscape++;
5270         } else {
5271             portrait++;
5272         }
5273     }
5274     return (landscape > portrait) ? QPageLayout::Landscape : QPageLayout::Portrait;
5275 }
5276 
5277 void Document::setAnnotationEditingEnabled(bool enable)
5278 {
5279     d->m_annotationEditingEnabled = enable;
5280     foreachObserver(notifySetup(d->m_pagesVector, 0));
5281 }
5282 
5283 void Document::walletDataForFile(const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey) const
5284 {
5285     if (d->m_generator) {
5286         d->m_generator->walletDataForFile(fileName, walletName, walletFolder, walletKey);
5287     } else if (d->m_walletGenerator) {
5288         d->m_walletGenerator->walletDataForFile(fileName, walletName, walletFolder, walletKey);
5289     }
5290 }
5291 
5292 bool Document::isDocdataMigrationNeeded() const
5293 {
5294     return d->m_docdataMigrationNeeded;
5295 }
5296 
5297 void Document::docdataMigrationDone()
5298 {
5299     if (d->m_docdataMigrationNeeded) {
5300         d->m_docdataMigrationNeeded = false;
5301         foreachObserver(notifySetup(d->m_pagesVector, 0));
5302     }
5303 }
5304 
5305 QAbstractItemModel *Document::layersModel() const
5306 {
5307     return d->m_generator ? d->m_generator->layersModel() : nullptr;
5308 }
5309 
5310 QString Document::openError() const
5311 {
5312     return d->m_openError;
5313 }
5314 
5315 QByteArray Document::requestSignedRevisionData(const Okular::SignatureInfo &info)
5316 {
5317     QFile f(d->m_docFileName);
5318     if (!f.open(QIODevice::ReadOnly)) {
5319         Q_EMIT error(i18n("Could not open '%1'. File does not exist", d->m_docFileName), -1);
5320         return {};
5321     }
5322 
5323     const QList<qint64> byteRange = info.signedRangeBounds();
5324     f.seek(byteRange.first());
5325     QByteArray data = f.read(byteRange.last() - byteRange.first());
5326     f.close();
5327 
5328     return data;
5329 }
5330 
5331 void Document::refreshPixmaps(int pageNumber)
5332 {
5333     d->refreshPixmaps(pageNumber);
5334 }
5335 
5336 void DocumentPrivate::executeScript(const QString &function)
5337 {
5338     if (!m_scripter) {
5339         m_scripter = new Scripter(this);
5340     }
5341     m_scripter->execute(JavaScript, function);
5342 }
5343 
5344 void DocumentPrivate::requestDone(PixmapRequest *req)
5345 {
5346     if (!req) {
5347         return;
5348     }
5349 
5350     if (!m_generator || m_closingLoop) {
5351         m_pixmapRequestsMutex.lock();
5352         m_executingPixmapRequests.remove(req);
5353         m_pixmapRequestsMutex.unlock();
5354         delete req;
5355         if (m_closingLoop) {
5356             m_closingLoop->exit();
5357         }
5358         return;
5359     }
5360 
5361 #ifndef NDEBUG
5362     if (!m_generator->canGeneratePixmap()) {
5363         qCDebug(OkularCoreDebug) << "requestDone with generator not in READY state.";
5364     }
5365 #endif
5366 
5367     if (!req->shouldAbortRender()) {
5368         // [MEM] 1.1 find and remove a previous entry for the same page and id
5369         std::list<AllocatedPixmap *>::iterator aIt = m_allocatedPixmaps.begin();
5370         std::list<AllocatedPixmap *>::iterator aEnd = m_allocatedPixmaps.end();
5371         for (; aIt != aEnd; ++aIt) {
5372             if ((*aIt)->page == req->pageNumber() && (*aIt)->observer == req->observer()) {
5373                 AllocatedPixmap *p = *aIt;
5374                 m_allocatedPixmaps.erase(aIt);
5375                 m_allocatedPixmapsTotalMemory -= p->memory;
5376                 delete p;
5377                 break;
5378             }
5379         }
5380 
5381         DocumentObserver *observer = req->observer();
5382         if (m_observers.contains(observer)) {
5383             // [MEM] 1.2 append memory allocation descriptor to the FIFO
5384             qulonglong memoryBytes = 0;
5385             const TilesManager *tm = req->d->tilesManager();
5386             if (tm) {
5387                 memoryBytes = tm->totalMemory();
5388             } else {
5389                 memoryBytes = 4 * req->width() * req->height();
5390             }
5391 
5392             AllocatedPixmap *memoryPage = new AllocatedPixmap(req->observer(), req->pageNumber(), memoryBytes);
5393             m_allocatedPixmaps.push_back(memoryPage);
5394             m_allocatedPixmapsTotalMemory += memoryBytes;
5395 
5396             // 2. notify an observer that its pixmap changed
5397             observer->notifyPageChanged(req->pageNumber(), DocumentObserver::Pixmap);
5398         }
5399 #ifndef NDEBUG
5400         else {
5401             qCWarning(OkularCoreDebug) << "Receiving a done request for the defunct observer" << observer;
5402         }
5403 #endif
5404     }
5405 
5406     // 3. delete request
5407     m_pixmapRequestsMutex.lock();
5408     m_executingPixmapRequests.remove(req);
5409     m_pixmapRequestsMutex.unlock();
5410     delete req;
5411 
5412     // 4. start a new generation if some is pending
5413     m_pixmapRequestsMutex.lock();
5414     bool hasPixmaps = !m_pixmapRequestsStack.empty();
5415     m_pixmapRequestsMutex.unlock();
5416     if (hasPixmaps) {
5417         sendGeneratorPixmapRequest();
5418     }
5419 }
5420 
5421 void DocumentPrivate::setPageBoundingBox(int page, const NormalizedRect &boundingBox)
5422 {
5423     Page *kp = m_pagesVector[page];
5424     if (!m_generator || !kp) {
5425         return;
5426     }
5427 
5428     if (kp->boundingBox() == boundingBox) {
5429         return;
5430     }
5431     kp->setBoundingBox(boundingBox);
5432 
5433     // notify observers about the change
5434     foreachObserverD(notifyPageChanged(page, DocumentObserver::BoundingBox));
5435 
5436     // TODO: For generators that generate the bbox by pixmap scanning, if the first generated pixmap is very small, the bounding box will forever be inaccurate.
5437     // TODO: Crop computation should also consider annotations, actions, etc. to make sure they're not cropped away.
5438     // TODO: Help compute bounding box for generators that create a QPixmap without a QImage, like text and plucker.
5439     // TODO: Don't compute the bounding box if no one needs it (e.g., Trim Borders is off).
5440 }
5441 
5442 void DocumentPrivate::calculateMaxTextPages()
5443 {
5444     int multipliers = qMax(1, qRound(getTotalMemory() / 536870912.0)); // 512 MB
5445     switch (SettingsCore::memoryLevel()) {
5446     case SettingsCore::EnumMemoryLevel::Low:
5447         m_maxAllocatedTextPages = multipliers * 2;
5448         break;
5449 
5450     case SettingsCore::EnumMemoryLevel::Normal:
5451         m_maxAllocatedTextPages = multipliers * 50;
5452         break;
5453 
5454     case SettingsCore::EnumMemoryLevel::Aggressive:
5455         m_maxAllocatedTextPages = multipliers * 250;
5456         break;
5457 
5458     case SettingsCore::EnumMemoryLevel::Greedy:
5459         m_maxAllocatedTextPages = multipliers * 1250;
5460         break;
5461     }
5462 }
5463 
5464 void DocumentPrivate::textGenerationDone(Page *page)
5465 {
5466     if (!m_pageController) {
5467         return;
5468     }
5469 
5470     // 1. If we reached the cache limit, delete the first text page from the fifo
5471     if (m_allocatedTextPagesFifo.size() == m_maxAllocatedTextPages) {
5472         int pageToKick = m_allocatedTextPagesFifo.takeFirst();
5473         if (pageToKick != page->number()) // this should never happen but better be safe than sorry
5474         {
5475             m_pagesVector.at(pageToKick)->setTextPage(nullptr); // deletes the textpage
5476         }
5477     }
5478 
5479     // 2. Add the page to the fifo of generated text pages
5480     m_allocatedTextPagesFifo.append(page->number());
5481 }
5482 
5483 void Document::setRotation(int r)
5484 {
5485     d->setRotationInternal(r, true);
5486 }
5487 
5488 void DocumentPrivate::setRotationInternal(int r, bool notify)
5489 {
5490     Rotation rotation = (Rotation)r;
5491     if (!m_generator || (m_rotation == rotation)) {
5492         return;
5493     }
5494 
5495     // tell the pages to rotate
5496     QVector<Okular::Page *>::const_iterator pIt = m_pagesVector.constBegin();
5497     QVector<Okular::Page *>::const_iterator pEnd = m_pagesVector.constEnd();
5498     for (; pIt != pEnd; ++pIt) {
5499         (*pIt)->d->rotateAt(rotation);
5500     }
5501     if (notify) {
5502         // notify the generator that the current rotation has changed
5503         m_generator->rotationChanged(rotation, m_rotation);
5504     }
5505     // set the new rotation
5506     m_rotation = rotation;
5507 
5508     if (notify) {
5509         foreachObserverD(notifySetup(m_pagesVector, DocumentObserver::NewLayoutForPages));
5510         foreachObserverD(notifyContentsCleared(DocumentObserver::Pixmap | DocumentObserver::Highlights | DocumentObserver::Annotations));
5511     }
5512     qCDebug(OkularCoreDebug) << "Rotated:" << r;
5513 }
5514 
5515 void Document::setPageSize(const PageSize &size)
5516 {
5517     if (!d->m_generator || !d->m_generator->hasFeature(Generator::PageSizes)) {
5518         return;
5519     }
5520 
5521     if (d->m_pageSizes.isEmpty()) {
5522         d->m_pageSizes = d->m_generator->pageSizes();
5523     }
5524     int sizeid = d->m_pageSizes.indexOf(size);
5525     if (sizeid == -1) {
5526         return;
5527     }
5528 
5529     // tell the pages to change size
5530     QVector<Okular::Page *>::const_iterator pIt = d->m_pagesVector.constBegin();
5531     QVector<Okular::Page *>::const_iterator pEnd = d->m_pagesVector.constEnd();
5532     for (; pIt != pEnd; ++pIt) {
5533         (*pIt)->d->changeSize(size);
5534     }
5535     // clear 'memory allocation' descriptors
5536     qDeleteAll(d->m_allocatedPixmaps);
5537     d->m_allocatedPixmaps.clear();
5538     d->m_allocatedPixmapsTotalMemory = 0;
5539     // notify the generator that the current page size has changed
5540     d->m_generator->pageSizeChanged(size, d->m_pageSize);
5541     // set the new page size
5542     d->m_pageSize = size;
5543 
5544     foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::NewLayoutForPages));
5545     foreachObserver(notifyContentsCleared(DocumentObserver::Pixmap | DocumentObserver::Highlights));
5546     qCDebug(OkularCoreDebug) << "New PageSize id:" << sizeid;
5547 }
5548 
5549 /** DocumentViewport **/
5550 
5551 DocumentViewport::DocumentViewport(int n)
5552     : pageNumber(n)
5553 {
5554     // default settings
5555     rePos.enabled = false;
5556     rePos.normalizedX = 0.5;
5557     rePos.normalizedY = 0.0;
5558     rePos.pos = Center;
5559     autoFit.enabled = false;
5560     autoFit.width = false;
5561     autoFit.height = false;
5562 }
5563 
5564 DocumentViewport::DocumentViewport(const QString &xmlDesc)
5565     : pageNumber(-1)
5566 {
5567     // default settings (maybe overridden below)
5568     rePos.enabled = false;
5569     rePos.normalizedX = 0.5;
5570     rePos.normalizedY = 0.0;
5571     rePos.pos = Center;
5572     autoFit.enabled = false;
5573     autoFit.width = false;
5574     autoFit.height = false;
5575 
5576     // check for string presence
5577     if (xmlDesc.isEmpty()) {
5578         return;
5579     }
5580 
5581     // decode the string
5582     bool ok;
5583     int field = 0;
5584     QString token = xmlDesc.section(QLatin1Char(';'), field, field);
5585     while (!token.isEmpty()) {
5586         // decode the current token
5587         if (field == 0) {
5588             pageNumber = token.toInt(&ok);
5589             if (!ok) {
5590                 return;
5591             }
5592         } else if (token.startsWith(QLatin1String("C1"))) {
5593             rePos.enabled = true;
5594             rePos.normalizedX = token.section(QLatin1Char(':'), 1, 1).toDouble();
5595             rePos.normalizedY = token.section(QLatin1Char(':'), 2, 2).toDouble();
5596             rePos.pos = Center;
5597         } else if (token.startsWith(QLatin1String("C2"))) {
5598             rePos.enabled = true;
5599             rePos.normalizedX = token.section(QLatin1Char(':'), 1, 1).toDouble();
5600             rePos.normalizedY = token.section(QLatin1Char(':'), 2, 2).toDouble();
5601             if (token.section(QLatin1Char(':'), 3, 3).toInt() == 1) {
5602                 rePos.pos = Center;
5603             } else {
5604                 rePos.pos = TopLeft;
5605             }
5606         } else if (token.startsWith(QLatin1String("AF1"))) {
5607             autoFit.enabled = true;
5608             autoFit.width = token.section(QLatin1Char(':'), 1, 1) == QLatin1String("T");
5609             autoFit.height = token.section(QLatin1Char(':'), 2, 2) == QLatin1String("T");
5610         }
5611         // proceed tokenizing string
5612         field++;
5613         token = xmlDesc.section(QLatin1Char(';'), field, field);
5614     }
5615 }
5616 
5617 QString DocumentViewport::toString() const
5618 {
5619     // start string with page number
5620     QString s = QString::number(pageNumber);
5621     // if has center coordinates, save them on string
5622     if (rePos.enabled) {
5623         s += QStringLiteral(";C2:") + QString::number(rePos.normalizedX) + QLatin1Char(':') + QString::number(rePos.normalizedY) + QLatin1Char(':') + QString::number(rePos.pos);
5624     }
5625     // if has autofit enabled, save its state on string
5626     if (autoFit.enabled) {
5627         s += QStringLiteral(";AF1:") + (autoFit.width ? QLatin1Char('T') : QLatin1Char('F')) + QLatin1Char(':') + (autoFit.height ? QLatin1Char('T') : QLatin1Char('F'));
5628     }
5629     return s;
5630 }
5631 
5632 bool DocumentViewport::isValid() const
5633 {
5634     return pageNumber >= 0;
5635 }
5636 
5637 bool DocumentViewport::operator==(const DocumentViewport &other) const
5638 {
5639     bool equal = (pageNumber == other.pageNumber) && (rePos.enabled == other.rePos.enabled) && (autoFit.enabled == other.autoFit.enabled);
5640     if (!equal) {
5641         return false;
5642     }
5643     if (rePos.enabled && ((rePos.normalizedX != other.rePos.normalizedX) || (rePos.normalizedY != other.rePos.normalizedY) || rePos.pos != other.rePos.pos)) {
5644         return false;
5645     }
5646     if (autoFit.enabled && ((autoFit.width != other.autoFit.width) || (autoFit.height != other.autoFit.height))) {
5647         return false;
5648     }
5649     return true;
5650 }
5651 
5652 bool DocumentViewport::operator<(const DocumentViewport &other) const
5653 {
5654     // TODO: Check autoFit and Position
5655 
5656     if (pageNumber != other.pageNumber) {
5657         return pageNumber < other.pageNumber;
5658     }
5659 
5660     if (!rePos.enabled && other.rePos.enabled) {
5661         return true;
5662     }
5663 
5664     if (!other.rePos.enabled) {
5665         return false;
5666     }
5667 
5668     if (rePos.normalizedY != other.rePos.normalizedY) {
5669         return rePos.normalizedY < other.rePos.normalizedY;
5670     }
5671 
5672     return rePos.normalizedX < other.rePos.normalizedX;
5673 }
5674 
5675 /** DocumentInfo **/
5676 
5677 DocumentInfo::DocumentInfo()
5678     : d(new DocumentInfoPrivate())
5679 {
5680 }
5681 
5682 DocumentInfo::DocumentInfo(const DocumentInfo &info)
5683     : d(new DocumentInfoPrivate())
5684 {
5685     *this = info;
5686 }
5687 
5688 DocumentInfo &DocumentInfo::operator=(const DocumentInfo &info)
5689 {
5690     if (this != &info) {
5691         d->values = info.d->values;
5692         d->titles = info.d->titles;
5693     }
5694     return *this;
5695 }
5696 
5697 DocumentInfo::~DocumentInfo()
5698 {
5699     delete d;
5700 }
5701 
5702 void DocumentInfo::set(const QString &key, const QString &value, const QString &title)
5703 {
5704     d->values[key] = value;
5705     d->titles[key] = title;
5706 }
5707 
5708 void DocumentInfo::set(Key key, const QString &value)
5709 {
5710     d->values[getKeyString(key)] = value;
5711 }
5712 
5713 QStringList DocumentInfo::keys() const
5714 {
5715     return d->values.keys();
5716 }
5717 
5718 QString DocumentInfo::get(Key key) const
5719 {
5720     return get(getKeyString(key));
5721 }
5722 
5723 QString DocumentInfo::get(const QString &key) const
5724 {
5725     return d->values[key];
5726 }
5727 
5728 QString DocumentInfo::getKeyString(Key key) // const
5729 {
5730     switch (key) {
5731     case Title:
5732         return QStringLiteral("title");
5733         break;
5734     case Subject:
5735         return QStringLiteral("subject");
5736         break;
5737     case Description:
5738         return QStringLiteral("description");
5739         break;
5740     case Author:
5741         return QStringLiteral("author");
5742         break;
5743     case Creator:
5744         return QStringLiteral("creator");
5745         break;
5746     case Producer:
5747         return QStringLiteral("producer");
5748         break;
5749     case Copyright:
5750         return QStringLiteral("copyright");
5751         break;
5752     case Pages:
5753         return QStringLiteral("pages");
5754         break;
5755     case CreationDate:
5756         return QStringLiteral("creationDate");
5757         break;
5758     case ModificationDate:
5759         return QStringLiteral("modificationDate");
5760         break;
5761     case MimeType:
5762         return QStringLiteral("mimeType");
5763         break;
5764     case Category:
5765         return QStringLiteral("category");
5766         break;
5767     case Keywords:
5768         return QStringLiteral("keywords");
5769         break;
5770     case FilePath:
5771         return QStringLiteral("filePath");
5772         break;
5773     case DocumentSize:
5774         return QStringLiteral("documentSize");
5775         break;
5776     case PagesSize:
5777         return QStringLiteral("pageSize");
5778         break;
5779     default:
5780         qCWarning(OkularCoreDebug) << "Unknown" << key;
5781         return QString();
5782         break;
5783     }
5784 }
5785 
5786 DocumentInfo::Key DocumentInfo::getKeyFromString(const QString &key) // const
5787 {
5788     if (key == QLatin1String("title")) {
5789         return Title;
5790     } else if (key == QLatin1String("subject")) {
5791         return Subject;
5792     } else if (key == QLatin1String("description")) {
5793         return Description;
5794     } else if (key == QLatin1String("author")) {
5795         return Author;
5796     } else if (key == QLatin1String("creator")) {
5797         return Creator;
5798     } else if (key == QLatin1String("producer")) {
5799         return Producer;
5800     } else if (key == QLatin1String("copyright")) {
5801         return Copyright;
5802     } else if (key == QLatin1String("pages")) {
5803         return Pages;
5804     } else if (key == QLatin1String("creationDate")) {
5805         return CreationDate;
5806     } else if (key == QLatin1String("modificationDate")) {
5807         return ModificationDate;
5808     } else if (key == QLatin1String("mimeType")) {
5809         return MimeType;
5810     } else if (key == QLatin1String("category")) {
5811         return Category;
5812     } else if (key == QLatin1String("keywords")) {
5813         return Keywords;
5814     } else if (key == QLatin1String("filePath")) {
5815         return FilePath;
5816     } else if (key == QLatin1String("documentSize")) {
5817         return DocumentSize;
5818     } else if (key == QLatin1String("pageSize")) {
5819         return PagesSize;
5820     } else {
5821         return Invalid;
5822     }
5823 }
5824 
5825 QString DocumentInfo::getKeyTitle(Key key) // const
5826 {
5827     switch (key) {
5828     case Title:
5829         return i18n("Title");
5830         break;
5831     case Subject:
5832         return i18n("Subject");
5833         break;
5834     case Description:
5835         return i18n("Description");
5836         break;
5837     case Author:
5838         return i18n("Author");
5839         break;
5840     case Creator:
5841         return i18n("Creator");
5842         break;
5843     case Producer:
5844         return i18n("Producer");
5845         break;
5846     case Copyright:
5847         return i18n("Copyright");
5848         break;
5849     case Pages:
5850         return i18n("Pages");
5851         break;
5852     case CreationDate:
5853         return i18n("Created");
5854         break;
5855     case ModificationDate:
5856         return i18n("Modified");
5857         break;
5858     case MimeType:
5859         return i18n("MIME Type");
5860         break;
5861     case Category:
5862         return i18n("Category");
5863         break;
5864     case Keywords:
5865         return i18n("Keywords");
5866         break;
5867     case FilePath:
5868         return i18n("File Path");
5869         break;
5870     case DocumentSize:
5871         return i18n("File Size");
5872         break;
5873     case PagesSize:
5874         return i18n("Page Size");
5875         break;
5876     default:
5877         return QString();
5878         break;
5879     }
5880 }
5881 
5882 QString DocumentInfo::getKeyTitle(const QString &key) const
5883 {
5884     QString title = getKeyTitle(getKeyFromString(key));
5885     if (title.isEmpty()) {
5886         title = d->titles[key];
5887     }
5888     return title;
5889 }
5890 
5891 /** DocumentSynopsis **/
5892 
5893 DocumentSynopsis::DocumentSynopsis()
5894     : QDomDocument(QStringLiteral("DocumentSynopsis"))
5895 {
5896     // void implementation, only subclassed for naming
5897 }
5898 
5899 DocumentSynopsis::DocumentSynopsis(const QDomDocument &document)
5900     : QDomDocument(document)
5901 {
5902 }
5903 
5904 /** EmbeddedFile **/
5905 
5906 EmbeddedFile::EmbeddedFile()
5907 {
5908 }
5909 
5910 EmbeddedFile::~EmbeddedFile()
5911 {
5912 }
5913 
5914 VisiblePageRect::VisiblePageRect(int page, const NormalizedRect &rectangle)
5915     : pageNumber(page)
5916     , rect(rectangle)
5917 {
5918 }
5919 
5920 /** NewSignatureData **/
5921 
5922 struct Okular::NewSignatureDataPrivate {
5923     NewSignatureDataPrivate() = default;
5924 
5925     QString certNickname;
5926     QString certSubjectCommonName;
5927     QString password;
5928     QString documentPassword;
5929     QString location;
5930     QString reason;
5931     QString backgroundImagePath;
5932     int page = -1;
5933     NormalizedRect boundingRectangle;
5934 };
5935 
5936 NewSignatureData::NewSignatureData()
5937     : d(new NewSignatureDataPrivate())
5938 {
5939 }
5940 
5941 NewSignatureData::~NewSignatureData()
5942 {
5943     delete d;
5944 }
5945 
5946 QString NewSignatureData::certNickname() const
5947 {
5948     return d->certNickname;
5949 }
5950 
5951 void NewSignatureData::setCertNickname(const QString &certNickname)
5952 {
5953     d->certNickname = certNickname;
5954 }
5955 
5956 QString NewSignatureData::certSubjectCommonName() const
5957 {
5958     return d->certSubjectCommonName;
5959 }
5960 
5961 void NewSignatureData::setCertSubjectCommonName(const QString &certSubjectCommonName)
5962 {
5963     d->certSubjectCommonName = certSubjectCommonName;
5964 }
5965 
5966 QString NewSignatureData::password() const
5967 {
5968     return d->password;
5969 }
5970 
5971 void NewSignatureData::setPassword(const QString &password)
5972 {
5973     d->password = password;
5974 }
5975 
5976 int NewSignatureData::page() const
5977 {
5978     return d->page;
5979 }
5980 
5981 void NewSignatureData::setPage(int page)
5982 {
5983     d->page = page;
5984 }
5985 
5986 NormalizedRect NewSignatureData::boundingRectangle() const
5987 {
5988     return d->boundingRectangle;
5989 }
5990 
5991 void NewSignatureData::setBoundingRectangle(const NormalizedRect &rect)
5992 {
5993     d->boundingRectangle = rect;
5994 }
5995 
5996 QString NewSignatureData::documentPassword() const
5997 {
5998     return d->documentPassword;
5999 }
6000 
6001 void NewSignatureData::setDocumentPassword(const QString &password)
6002 {
6003     d->documentPassword = password;
6004 }
6005 
6006 QString NewSignatureData::location() const
6007 {
6008     return d->location;
6009 }
6010 
6011 void NewSignatureData::setLocation(const QString &location)
6012 {
6013     d->location = location;
6014 }
6015 
6016 QString NewSignatureData::reason() const
6017 {
6018     return d->reason;
6019 }
6020 
6021 void NewSignatureData::setReason(const QString &reason)
6022 {
6023     d->reason = reason;
6024 }
6025 
6026 QString Okular::NewSignatureData::backgroundImagePath() const
6027 {
6028     return d->backgroundImagePath;
6029 }
6030 
6031 void Okular::NewSignatureData::setBackgroundImagePath(const QString &path)
6032 {
6033     d->backgroundImagePath = path;
6034 }
6035 
6036 #undef foreachObserver
6037 #undef foreachObserverD
6038 
6039 #include "document.moc"
6040 
6041 /* kate: replace-tabs on; indent-width 4; */