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; */