1 /*
2     SPDX-FileCopyrightText: 2004-2005 Enrico Ros <eros.kde@email.it>
3     SPDX-FileCopyrightText: 2004-2008 Albert Astals Cid <aacid@kde.org>
4 
5     Work sponsored by the LiMux project of the city of Munich:
6     SPDX-FileCopyrightText: 2017, 2018 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
7 
8     SPDX-License-Identifier: GPL-2.0-or-later
9 */
10 
11 #include "document.h"
12 #include "document_p.h"
13 #include "documentcommands_p.h"
14 
15 #include <limits.h>
16 #include <memory>
17 #ifdef Q_OS_WIN
18 #define _WIN32_WINNT 0x0500
19 #include <windows.h>
20 #elif defined(Q_OS_FREEBSD)
21 // clang-format off
22 // FreeBSD really wants this include order
23 #include <sys/types.h>
24 #include <sys/sysctl.h>
25 // clang-format on
26 #include <vm/vm_param.h>
27 #endif
28 
29 // qt/kde/system includes
30 #include <QApplication>
31 #include <QDesktopServices>
32 #include <QDir>
33 #include <QFile>
34 #include <QFileInfo>
35 #include <QLabel>
36 #include <QMap>
37 #include <QMimeDatabase>
38 #include <QPageSize>
39 #include <QPrintDialog>
40 #include <QRegularExpression>
41 #include <QScreen>
42 #include <QStack>
43 #include <QStandardPaths>
44 #include <QTemporaryFile>
45 #include <QTextStream>
46 #include <QTimer>
47 #include <QUndoCommand>
48 #include <QWindow>
49 #include <QtAlgorithms>
50 
51 #include <KAuthorized>
52 #include <KConfigDialog>
53 #include <KFormat>
54 #include <KIO/Global>
55 #include <KLocalizedString>
56 #include <KMacroExpander>
57 #include <KApplicationTrader>
58 #include <KPluginMetaData>
59 #include <KProcess>
60 #include <KRun>
61 #include <KShell>
62 #include <Kdelibs4Migration>
63 #include <kzip.h>
64 
65 // local includes
66 #include "action.h"
67 #include "annotations.h"
68 #include "annotations_p.h"
69 #include "audioplayer.h"
70 #include "audioplayer_p.h"
71 #include "bookmarkmanager.h"
72 #include "chooseenginedialog_p.h"
73 #include "debug_p.h"
74 #include "form.h"
75 #include "generator_p.h"
76 #include "interfaces/configinterface.h"
77 #include "interfaces/guiinterface.h"
78 #include "interfaces/printinterface.h"
79 #include "interfaces/saveinterface.h"
80 #include "misc.h"
81 #include "observer.h"
82 #include "page.h"
83 #include "page_p.h"
84 #include "pagecontroller_p.h"
85 #include "script/event_p.h"
86 #include "scripter.h"
87 #include "settings_core.h"
88 #include "sourcereference.h"
89 #include "sourcereference_p.h"
90 #include "texteditors_p.h"
91 #include "tile.h"
92 #include "tilesmanager_p.h"
93 #include "utils.h"
94 #include "utils_p.h"
95 #include "view.h"
96 #include "view_p.h"
97 
98 #include <config-okular.h>
99 
100 #if HAVE_MALLOC_TRIM
101 #include "malloc.h"
102 #endif
103 
104 using namespace Okular;
105 
106 struct AllocatedPixmap {
107     // owner of the page
108     DocumentObserver *observer;
109     int page;
110     qulonglong memory;
111     // public constructor: initialize data
AllocatedPixmapAllocatedPixmap112     AllocatedPixmap(DocumentObserver *o, int p, qulonglong m)
113         : observer(o)
114         , page(p)
115         , memory(m)
116     {
117     }
118 };
119 
120 struct ArchiveData {
ArchiveDataArchiveData121     ArchiveData()
122     {
123     }
124 
125     QString originalFileName;
126     QTemporaryFile document;
127     QTemporaryFile metadataFile;
128 };
129 
130 struct RunningSearch {
131     // store search properties
132     int continueOnPage;
133     RegularAreaRect continueOnMatch;
134     QSet<int> highlightedPages;
135 
136     // fields related to previous searches (used for 'continueSearch')
137     QString cachedString;
138     Document::SearchType cachedType;
139     Qt::CaseSensitivity cachedCaseSensitivity;
140     bool cachedViewportMove : 1;
141     bool isCurrentlySearching : 1;
142     QColor cachedColor;
143     int pagesDone;
144 };
145 
146 #define foreachObserver(cmd)                                                                                                                                                                                                                   \
147     {                                                                                                                                                                                                                                          \
148         QSet<DocumentObserver *>::const_iterator it = d->m_observers.constBegin(), end = d->m_observers.constEnd();                                                                                                                            \
149         for (; it != end; ++it) {                                                                                                                                                                                                              \
150             (*it)->cmd;                                                                                                                                                                                                                        \
151         }                                                                                                                                                                                                                                      \
152     }
153 
154 #define foreachObserverD(cmd)                                                                                                                                                                                                                  \
155     {                                                                                                                                                                                                                                          \
156         QSet<DocumentObserver *>::const_iterator it = m_observers.constBegin(), end = m_observers.constEnd();                                                                                                                                  \
157         for (; it != end; ++it) {                                                                                                                                                                                                              \
158             (*it)->cmd;                                                                                                                                                                                                                        \
159         }                                                                                                                                                                                                                                      \
160     }
161 
162 #define OKULAR_HISTORY_MAXSTEPS 100
163 #define OKULAR_HISTORY_SAVEDSTEPS 10
164 
165 // how often to run slotTimedMemoryCheck
166 const int kMemCheckTime = 2000; // in msec
167 
168 /***** Document ******/
169 
pagesSizeString() const170 QString DocumentPrivate::pagesSizeString() const
171 {
172     if (m_generator) {
173         if (m_generator->pagesSizeMetric() != Generator::None) {
174             QSizeF size = m_parent->allPagesSize();
175             // Single page size
176             if (size.isValid())
177                 return localizedSize(size);
178 
179             // Multiple page sizes
180             QString sizeString;
181             QHash<QString, int> pageSizeFrequencies;
182 
183             // Compute frequencies of each page size
184             for (int i = 0; i < m_pagesVector.count(); ++i) {
185                 const Page *p = m_pagesVector.at(i);
186                 sizeString = localizedSize(QSizeF(p->width(), p->height()));
187                 pageSizeFrequencies[sizeString] = pageSizeFrequencies.value(sizeString, 0) + 1;
188             }
189 
190             // Figure out which page size is most frequent
191             int largestFrequencySeen = 0;
192             QString mostCommonPageSize = QString();
193             QHash<QString, int>::const_iterator i = pageSizeFrequencies.constBegin();
194             while (i != pageSizeFrequencies.constEnd()) {
195                 if (i.value() > largestFrequencySeen) {
196                     largestFrequencySeen = i.value();
197                     mostCommonPageSize = i.key();
198                 }
199                 ++i;
200             }
201             QString finalText = i18nc("@info %1 is a page size", "Most pages are %1.", mostCommonPageSize);
202 
203             return finalText;
204         } else
205             return QString();
206     } else
207         return QString();
208 }
209 
namePaperSize(double inchesWidth,double inchesHeight) const210 QString DocumentPrivate::namePaperSize(double inchesWidth, double inchesHeight) const
211 {
212     const QPrinter::Orientation orientation = inchesWidth > inchesHeight ? QPrinter::Landscape : QPrinter::Portrait;
213 
214     const QSize pointsSize(inchesWidth * 72.0, inchesHeight * 72.0);
215     const QPageSize::PageSizeId paperSize = QPageSize::id(pointsSize, QPageSize::FuzzyOrientationMatch);
216 
217     const QString paperName = QPageSize::name(paperSize);
218 
219     if (orientation == QPrinter::Portrait) {
220         return i18nc("paper type and orientation (eg: Portrait A4)", "Portrait %1", paperName);
221     } else {
222         return i18nc("paper type and orientation (eg: Portrait A4)", "Landscape %1", paperName);
223     }
224 }
225 
localizedSize(const QSizeF size) const226 QString DocumentPrivate::localizedSize(const QSizeF size) const
227 {
228     double inchesWidth = 0, inchesHeight = 0;
229     switch (m_generator->pagesSizeMetric()) {
230     case Generator::Points:
231         inchesWidth = size.width() / 72.0;
232         inchesHeight = size.height() / 72.0;
233         break;
234 
235     case Generator::Pixels: {
236         const QSizeF dpi = m_generator->dpi();
237         inchesWidth = size.width() / dpi.width();
238         inchesHeight = size.height() / dpi.height();
239     } break;
240 
241     case Generator::None:
242         break;
243     }
244     if (QLocale::system().measurementSystem() == QLocale::ImperialSystem) {
245         return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 in (%3)", inchesWidth, inchesHeight, namePaperSize(inchesWidth, inchesHeight));
246     } else {
247         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));
248     }
249 }
250 
calculateMemoryToFree()251 qulonglong DocumentPrivate::calculateMemoryToFree()
252 {
253     // [MEM] choose memory parameters based on configuration profile
254     qulonglong clipValue = 0;
255     qulonglong memoryToFree = 0;
256 
257     switch (SettingsCore::memoryLevel()) {
258     case SettingsCore::EnumMemoryLevel::Low:
259         memoryToFree = m_allocatedPixmapsTotalMemory;
260         break;
261 
262     case SettingsCore::EnumMemoryLevel::Normal: {
263         qulonglong thirdTotalMemory = getTotalMemory() / 3;
264         qulonglong freeMemory = getFreeMemory();
265         if (m_allocatedPixmapsTotalMemory > thirdTotalMemory)
266             memoryToFree = m_allocatedPixmapsTotalMemory - thirdTotalMemory;
267         if (m_allocatedPixmapsTotalMemory > freeMemory)
268             clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2;
269     } break;
270 
271     case SettingsCore::EnumMemoryLevel::Aggressive: {
272         qulonglong freeMemory = getFreeMemory();
273         if (m_allocatedPixmapsTotalMemory > freeMemory)
274             clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2;
275     } break;
276     case SettingsCore::EnumMemoryLevel::Greedy: {
277         qulonglong freeSwap;
278         qulonglong freeMemory = getFreeMemory(&freeSwap);
279         const qulonglong memoryLimit = qMin(qMax(freeMemory, getTotalMemory() / 2), freeMemory + freeSwap);
280         if (m_allocatedPixmapsTotalMemory > memoryLimit)
281             clipValue = (m_allocatedPixmapsTotalMemory - memoryLimit) / 2;
282     } break;
283     }
284 
285     if (clipValue > memoryToFree)
286         memoryToFree = clipValue;
287 
288     return memoryToFree;
289 }
290 
cleanupPixmapMemory()291 void DocumentPrivate::cleanupPixmapMemory()
292 {
293     cleanupPixmapMemory(calculateMemoryToFree());
294 }
295 
cleanupPixmapMemory(qulonglong memoryToFree)296 void DocumentPrivate::cleanupPixmapMemory(qulonglong memoryToFree)
297 {
298     if (memoryToFree < 1)
299         return;
300 
301     const int currentViewportPage = (*m_viewportIterator).pageNumber;
302 
303     // Create a QMap of visible rects, indexed by page number
304     QMap<int, VisiblePageRect *> visibleRects;
305     QVector<Okular::VisiblePageRect *>::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd();
306     for (; vIt != vEnd; ++vIt)
307         visibleRects.insert((*vIt)->pageNumber, (*vIt));
308 
309     // Free memory starting from pages that are farthest from the current one
310     int pagesFreed = 0;
311     while (memoryToFree > 0) {
312         AllocatedPixmap *p = searchLowestPriorityPixmap(true, true);
313         if (!p) // No pixmap to remove
314             break;
315 
316         qCDebug(OkularCoreDebug).nospace() << "Evicting cache pixmap observer=" << p->observer << " page=" << p->page;
317 
318         // m_allocatedPixmapsTotalMemory can't underflow because we always add or remove
319         // the memory used by the AllocatedPixmap so at most it can reach zero
320         m_allocatedPixmapsTotalMemory -= p->memory;
321         // Make sure memoryToFree does not underflow
322         if (p->memory > memoryToFree)
323             memoryToFree = 0;
324         else
325             memoryToFree -= p->memory;
326         pagesFreed++;
327         // delete pixmap
328         m_pagesVector.at(p->page)->deletePixmap(p->observer);
329         // delete allocation descriptor
330         delete p;
331     }
332 
333     // If we're still on low memory, try to free individual tiles
334 
335     // Store pages that weren't completely removed
336 
337     QLinkedList<AllocatedPixmap *> pixmapsToKeep;
338     while (memoryToFree > 0) {
339         int clean_hits = 0;
340         for (DocumentObserver *observer : qAsConst(m_observers)) {
341             AllocatedPixmap *p = searchLowestPriorityPixmap(false, true, observer);
342             if (!p) // No pixmap to remove
343                 continue;
344 
345             clean_hits++;
346 
347             TilesManager *tilesManager = m_pagesVector.at(p->page)->d->tilesManager(observer);
348             if (tilesManager && tilesManager->totalMemory() > 0) {
349                 qulonglong memoryDiff = p->memory;
350                 NormalizedRect visibleRect;
351                 if (visibleRects.contains(p->page))
352                     visibleRect = visibleRects[p->page]->rect;
353 
354                 // Free non visible tiles
355                 tilesManager->cleanupPixmapMemory(memoryToFree, visibleRect, currentViewportPage);
356 
357                 p->memory = tilesManager->totalMemory();
358                 memoryDiff -= p->memory;
359                 memoryToFree = (memoryDiff < memoryToFree) ? (memoryToFree - memoryDiff) : 0;
360                 m_allocatedPixmapsTotalMemory -= memoryDiff;
361 
362                 if (p->memory > 0)
363                     pixmapsToKeep.append(p);
364                 else
365                     delete p;
366             } else
367                 pixmapsToKeep.append(p);
368         }
369 
370         if (clean_hits == 0)
371             break;
372     }
373 
374     m_allocatedPixmaps += pixmapsToKeep;
375     // p--rintf("freeMemory A:[%d -%d = %d] \n", m_allocatedPixmaps.count() + pagesFreed, pagesFreed, m_allocatedPixmaps.count() );
376 }
377 
378 /* Returns the next pixmap to evict from cache, or NULL if no suitable pixmap
379  * if found. If unloadableOnly is set, only unloadable pixmaps are returned. If
380  * thenRemoveIt is set, the pixmap is removed from m_allocatedPixmaps before
381  * returning it
382  */
searchLowestPriorityPixmap(bool unloadableOnly,bool thenRemoveIt,DocumentObserver * observer)383 AllocatedPixmap *DocumentPrivate::searchLowestPriorityPixmap(bool unloadableOnly, bool thenRemoveIt, DocumentObserver *observer)
384 {
385     QLinkedList<AllocatedPixmap *>::iterator pIt = m_allocatedPixmaps.begin();
386     QLinkedList<AllocatedPixmap *>::iterator pEnd = m_allocatedPixmaps.end();
387     QLinkedList<AllocatedPixmap *>::iterator farthestPixmap = pEnd;
388     const int currentViewportPage = (*m_viewportIterator).pageNumber;
389 
390     /* Find the pixmap that is farthest from the current viewport */
391     int maxDistance = -1;
392     while (pIt != pEnd) {
393         const AllocatedPixmap *p = *pIt;
394         // Filter by observer
395         if (observer == nullptr || p->observer == observer) {
396             const int distance = qAbs(p->page - currentViewportPage);
397             if (maxDistance < distance && (!unloadableOnly || p->observer->canUnloadPixmap(p->page))) {
398                 maxDistance = distance;
399                 farthestPixmap = pIt;
400             }
401         }
402         ++pIt;
403     }
404 
405     /* No pixmap to remove */
406     if (farthestPixmap == pEnd)
407         return nullptr;
408 
409     AllocatedPixmap *selectedPixmap = *farthestPixmap;
410     if (thenRemoveIt)
411         m_allocatedPixmaps.erase(farthestPixmap);
412     return selectedPixmap;
413 }
414 
getTotalMemory()415 qulonglong DocumentPrivate::getTotalMemory()
416 {
417     static qulonglong cachedValue = 0;
418     if (cachedValue)
419         return cachedValue;
420 
421 #if defined(Q_OS_LINUX)
422     // if /proc/meminfo doesn't exist, return 128MB
423     QFile memFile(QStringLiteral("/proc/meminfo"));
424     if (!memFile.open(QIODevice::ReadOnly))
425         return (cachedValue = 134217728);
426 
427     QTextStream readStream(&memFile);
428     while (true) {
429         QString entry = readStream.readLine();
430         if (entry.isNull())
431             break;
432         if (entry.startsWith(QLatin1String("MemTotal:")))
433             return (cachedValue = (Q_UINT64_C(1024) * entry.section(QLatin1Char(' '), -2, -2).toULongLong()));
434     }
435 #elif defined(Q_OS_FREEBSD)
436     qulonglong physmem;
437     int mib[] = {CTL_HW, HW_PHYSMEM};
438     size_t len = sizeof(physmem);
439     if (sysctl(mib, 2, &physmem, &len, NULL, 0) == 0)
440         return (cachedValue = physmem);
441 #elif defined(Q_OS_WIN)
442     MEMORYSTATUSEX stat;
443     stat.dwLength = sizeof(stat);
444     GlobalMemoryStatusEx(&stat);
445 
446     return (cachedValue = stat.ullTotalPhys);
447 #endif
448     return (cachedValue = 134217728);
449 }
450 
getFreeMemory(qulonglong * freeSwap)451 qulonglong DocumentPrivate::getFreeMemory(qulonglong *freeSwap)
452 {
453     static QTime lastUpdate = QTime::currentTime().addSecs(-3);
454     static qulonglong cachedValue = 0;
455     static qulonglong cachedFreeSwap = 0;
456 
457     if (qAbs(lastUpdate.msecsTo(QTime::currentTime())) <= kMemCheckTime - 100) {
458         if (freeSwap)
459             *freeSwap = cachedFreeSwap;
460         return cachedValue;
461     }
462 
463     /* Initialize the returned free swap value to 0. It is overwritten if the
464      * actual value is available */
465     if (freeSwap)
466         *freeSwap = 0;
467 
468 #if defined(Q_OS_LINUX)
469     // if /proc/meminfo doesn't exist, return MEMORY FULL
470     QFile memFile(QStringLiteral("/proc/meminfo"));
471     if (!memFile.open(QIODevice::ReadOnly))
472         return 0;
473 
474     // read /proc/meminfo and sum up the contents of 'MemFree', 'Buffers'
475     // and 'Cached' fields. consider swapped memory as used memory.
476     qulonglong memoryFree = 0;
477     QString entry;
478     QTextStream readStream(&memFile);
479     static const int nElems = 5;
480     QString names[nElems] = {QStringLiteral("MemFree:"), QStringLiteral("Buffers:"), QStringLiteral("Cached:"), QStringLiteral("SwapFree:"), QStringLiteral("SwapTotal:")};
481     qulonglong values[nElems] = {0, 0, 0, 0, 0};
482     bool foundValues[nElems] = {false, false, false, false, false};
483     while (true) {
484         entry = readStream.readLine();
485         if (entry.isNull())
486             break;
487         for (int i = 0; i < nElems; ++i) {
488             if (entry.startsWith(names[i])) {
489                 values[i] = entry.section(QLatin1Char(' '), -2, -2).toULongLong(&foundValues[i]);
490             }
491         }
492     }
493     memFile.close();
494     bool found = true;
495     for (int i = 0; found && i < nElems; ++i)
496         found = found && foundValues[i];
497     if (found) {
498         /* MemFree + Buffers + Cached - SwapUsed =
499          * = MemFree + Buffers + Cached - (SwapTotal - SwapFree) =
500          * = MemFree + Buffers + Cached + SwapFree - SwapTotal */
501         memoryFree = values[0] + values[1] + values[2] + values[3];
502         if (values[4] > memoryFree)
503             memoryFree = 0;
504         else
505             memoryFree -= values[4];
506     } else {
507         return 0;
508     }
509 
510     lastUpdate = QTime::currentTime();
511 
512     if (freeSwap)
513         *freeSwap = (cachedFreeSwap = (Q_UINT64_C(1024) * values[3]));
514     return (cachedValue = (Q_UINT64_C(1024) * memoryFree));
515 #elif defined(Q_OS_FREEBSD)
516     qulonglong cache, inact, free, psize;
517     size_t cachelen, inactlen, freelen, psizelen;
518     cachelen = sizeof(cache);
519     inactlen = sizeof(inact);
520     freelen = sizeof(free);
521     psizelen = sizeof(psize);
522     // sum up inactive, cached and free memory
523     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 &&
524         sysctlbyname("vm.stats.vm.v_free_count", &free, &freelen, NULL, 0) == 0 && sysctlbyname("vm.stats.vm.v_page_size", &psize, &psizelen, NULL, 0) == 0) {
525         lastUpdate = QTime::currentTime();
526         return (cachedValue = (cache + inact + free) * psize);
527     } else {
528         return 0;
529     }
530 #elif defined(Q_OS_WIN)
531     MEMORYSTATUSEX stat;
532     stat.dwLength = sizeof(stat);
533     GlobalMemoryStatusEx(&stat);
534 
535     lastUpdate = QTime::currentTime();
536 
537     if (freeSwap)
538         *freeSwap = (cachedFreeSwap = stat.ullAvailPageFile);
539     return (cachedValue = stat.ullAvailPhys);
540 #else
541     // tell the memory is full.. will act as in LOW profile
542     return 0;
543 #endif
544 }
545 
loadDocumentInfo(LoadDocumentInfoFlags loadWhat)546 bool DocumentPrivate::loadDocumentInfo(LoadDocumentInfoFlags loadWhat)
547 // note: load data and stores it internally (document or pages). observers
548 // are still uninitialized at this point so don't access them
549 {
550     // qCDebug(OkularCoreDebug).nospace() << "Using '" << d->m_xmlFileName << "' as document info file.";
551     if (m_xmlFileName.isEmpty())
552         return false;
553 
554     QFile infoFile(m_xmlFileName);
555     return loadDocumentInfo(infoFile, loadWhat);
556 }
557 
loadDocumentInfo(QFile & infoFile,LoadDocumentInfoFlags loadWhat)558 bool DocumentPrivate::loadDocumentInfo(QFile &infoFile, LoadDocumentInfoFlags loadWhat)
559 {
560     if (!infoFile.exists() || !infoFile.open(QIODevice::ReadOnly))
561         return false;
562 
563     // Load DOM from XML file
564     QDomDocument doc(QStringLiteral("documentInfo"));
565     if (!doc.setContent(&infoFile)) {
566         qCDebug(OkularCoreDebug) << "Can't load XML pair! Check for broken xml.";
567         infoFile.close();
568         return false;
569     }
570     infoFile.close();
571 
572     QDomElement root = doc.documentElement();
573 
574     if (root.tagName() != QLatin1String("documentInfo"))
575         return false;
576 
577     bool loadedAnything = false; // set if something gets actually loaded
578 
579     // Parse the DOM tree
580     QDomNode topLevelNode = root.firstChild();
581     while (topLevelNode.isElement()) {
582         QString catName = topLevelNode.toElement().tagName();
583 
584         // Restore page attributes (bookmark, annotations, ...) from the DOM
585         if (catName == QLatin1String("pageList") && (loadWhat & LoadPageInfo)) {
586             QDomNode pageNode = topLevelNode.firstChild();
587             while (pageNode.isElement()) {
588                 QDomElement pageElement = pageNode.toElement();
589                 if (pageElement.hasAttribute(QStringLiteral("number"))) {
590                     // get page number (node's attribute)
591                     bool ok;
592                     int pageNumber = pageElement.attribute(QStringLiteral("number")).toInt(&ok);
593 
594                     // pass the domElement to the right page, to read config data from
595                     if (ok && pageNumber >= 0 && pageNumber < (int)m_pagesVector.count()) {
596                         if (m_pagesVector[pageNumber]->d->restoreLocalContents(pageElement))
597                             loadedAnything = true;
598                     }
599                 }
600                 pageNode = pageNode.nextSibling();
601             }
602         }
603 
604         // Restore 'general info' from the DOM
605         else if (catName == QLatin1String("generalInfo") && (loadWhat & LoadGeneralInfo)) {
606             QDomNode infoNode = topLevelNode.firstChild();
607             while (infoNode.isElement()) {
608                 QDomElement infoElement = infoNode.toElement();
609 
610                 // restore viewports history
611                 if (infoElement.tagName() == QLatin1String("history")) {
612                     // clear history
613                     m_viewportHistory.clear();
614                     // append old viewports
615                     QDomNode historyNode = infoNode.firstChild();
616                     while (historyNode.isElement()) {
617                         QDomElement historyElement = historyNode.toElement();
618                         if (historyElement.hasAttribute(QStringLiteral("viewport"))) {
619                             QString vpString = historyElement.attribute(QStringLiteral("viewport"));
620                             m_viewportIterator = m_viewportHistory.insert(m_viewportHistory.end(), DocumentViewport(vpString));
621                             loadedAnything = true;
622                         }
623                         historyNode = historyNode.nextSibling();
624                     }
625                     // consistency check
626                     if (m_viewportHistory.isEmpty())
627                         m_viewportIterator = m_viewportHistory.insert(m_viewportHistory.end(), DocumentViewport());
628                 } else if (infoElement.tagName() == QLatin1String("rotation")) {
629                     QString str = infoElement.text();
630                     bool ok = true;
631                     int newrotation = !str.isEmpty() ? (str.toInt(&ok) % 4) : 0;
632                     if (ok && newrotation != 0) {
633                         setRotationInternal(newrotation, false);
634                         loadedAnything = true;
635                     }
636                 } else if (infoElement.tagName() == QLatin1String("views")) {
637                     QDomNode viewNode = infoNode.firstChild();
638                     while (viewNode.isElement()) {
639                         QDomElement viewElement = viewNode.toElement();
640                         if (viewElement.tagName() == QLatin1String("view")) {
641                             const QString viewName = viewElement.attribute(QStringLiteral("name"));
642                             for (View *view : qAsConst(m_views)) {
643                                 if (view->name() == viewName) {
644                                     loadViewsInfo(view, viewElement);
645                                     loadedAnything = true;
646                                     break;
647                                 }
648                             }
649                         }
650                         viewNode = viewNode.nextSibling();
651                     }
652                 }
653                 infoNode = infoNode.nextSibling();
654             }
655         }
656 
657         topLevelNode = topLevelNode.nextSibling();
658     } // </documentInfo>
659 
660     return loadedAnything;
661 }
662 
loadViewsInfo(View * view,const QDomElement & e)663 void DocumentPrivate::loadViewsInfo(View *view, const QDomElement &e)
664 {
665     QDomNode viewNode = e.firstChild();
666     while (viewNode.isElement()) {
667         QDomElement viewElement = viewNode.toElement();
668 
669         if (viewElement.tagName() == QLatin1String("zoom")) {
670             const QString valueString = viewElement.attribute(QStringLiteral("value"));
671             bool newzoom_ok = true;
672             const double newzoom = !valueString.isEmpty() ? valueString.toDouble(&newzoom_ok) : 1.0;
673             if (newzoom_ok && newzoom != 0 && view->supportsCapability(View::Zoom) && (view->capabilityFlags(View::Zoom) & (View::CapabilityRead | View::CapabilitySerializable))) {
674                 view->setCapability(View::Zoom, newzoom);
675             }
676             const QString modeString = viewElement.attribute(QStringLiteral("mode"));
677             bool newmode_ok = true;
678             const int newmode = !modeString.isEmpty() ? modeString.toInt(&newmode_ok) : 2;
679             if (newmode_ok && view->supportsCapability(View::ZoomModality) && (view->capabilityFlags(View::ZoomModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
680                 view->setCapability(View::ZoomModality, newmode);
681             }
682         } else if (viewElement.tagName() == QLatin1String("viewMode")) {
683             const QString modeString = viewElement.attribute(QStringLiteral("mode"));
684             bool newmode_ok = true;
685             const int newmode = !modeString.isEmpty() ? modeString.toInt(&newmode_ok) : 2;
686             if (newmode_ok && view->supportsCapability(View::ViewModeModality) && (view->capabilityFlags(View::ViewModeModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
687                 view->setCapability(View::ViewModeModality, newmode);
688             }
689         } else if (viewElement.tagName() == QLatin1String("continuous")) {
690             const QString modeString = viewElement.attribute(QStringLiteral("mode"));
691             bool newmode_ok = true;
692             const int newmode = !modeString.isEmpty() ? modeString.toInt(&newmode_ok) : 2;
693             if (newmode_ok && view->supportsCapability(View::Continuous) && (view->capabilityFlags(View::Continuous) & (View::CapabilityRead | View::CapabilitySerializable))) {
694                 view->setCapability(View::Continuous, newmode);
695             }
696         } else if (viewElement.tagName() == QLatin1String("trimMargins")) {
697             const QString valueString = viewElement.attribute(QStringLiteral("value"));
698             bool newmode_ok = true;
699             const int newmode = !valueString.isEmpty() ? valueString.toInt(&newmode_ok) : 2;
700             if (newmode_ok && view->supportsCapability(View::TrimMargins) && (view->capabilityFlags(View::TrimMargins) & (View::CapabilityRead | View::CapabilitySerializable))) {
701                 view->setCapability(View::TrimMargins, newmode);
702             }
703         }
704 
705         viewNode = viewNode.nextSibling();
706     }
707 }
708 
saveViewsInfo(View * view,QDomElement & e) const709 void DocumentPrivate::saveViewsInfo(View *view, QDomElement &e) const
710 {
711     if (view->supportsCapability(View::Zoom) && (view->capabilityFlags(View::Zoom) & (View::CapabilityRead | View::CapabilitySerializable)) && view->supportsCapability(View::ZoomModality) &&
712         (view->capabilityFlags(View::ZoomModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
713         QDomElement zoomEl = e.ownerDocument().createElement(QStringLiteral("zoom"));
714         e.appendChild(zoomEl);
715         bool ok = true;
716         const double zoom = view->capability(View::Zoom).toDouble(&ok);
717         if (ok && zoom != 0) {
718             zoomEl.setAttribute(QStringLiteral("value"), QString::number(zoom));
719         }
720         const int mode = view->capability(View::ZoomModality).toInt(&ok);
721         if (ok) {
722             zoomEl.setAttribute(QStringLiteral("mode"), mode);
723         }
724     }
725     if (view->supportsCapability(View::Continuous) && (view->capabilityFlags(View::Continuous) & (View::CapabilityRead | View::CapabilitySerializable))) {
726         QDomElement contEl = e.ownerDocument().createElement(QStringLiteral("continuous"));
727         e.appendChild(contEl);
728         const bool mode = view->capability(View::Continuous).toBool();
729         contEl.setAttribute(QStringLiteral("mode"), mode);
730     }
731     if (view->supportsCapability(View::ViewModeModality) && (view->capabilityFlags(View::ViewModeModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
732         QDomElement viewEl = e.ownerDocument().createElement(QStringLiteral("viewMode"));
733         e.appendChild(viewEl);
734         bool ok = true;
735         const int mode = view->capability(View::ViewModeModality).toInt(&ok);
736         if (ok) {
737             viewEl.setAttribute(QStringLiteral("mode"), mode);
738         }
739     }
740     if (view->supportsCapability(View::TrimMargins) && (view->capabilityFlags(View::TrimMargins) & (View::CapabilityRead | View::CapabilitySerializable))) {
741         QDomElement contEl = e.ownerDocument().createElement(QStringLiteral("trimMargins"));
742         e.appendChild(contEl);
743         const bool value = view->capability(View::TrimMargins).toBool();
744         contEl.setAttribute(QStringLiteral("value"), value);
745     }
746 }
747 
giveAbsoluteUrl(const QString & fileName) const748 QUrl DocumentPrivate::giveAbsoluteUrl(const QString &fileName) const
749 {
750     if (!QDir::isRelativePath(fileName))
751         return QUrl::fromLocalFile(fileName);
752 
753     if (!m_url.isValid())
754         return QUrl();
755 
756     return QUrl(KIO::upUrl(m_url).toString() + fileName);
757 }
758 
openRelativeFile(const QString & fileName)759 bool DocumentPrivate::openRelativeFile(const QString &fileName)
760 {
761     const QUrl newUrl = giveAbsoluteUrl(fileName);
762     if (newUrl.isEmpty())
763         return false;
764 
765     qCDebug(OkularCoreDebug).nospace() << "openRelativeFile: '" << newUrl << "'";
766 
767     emit m_parent->openUrl(newUrl);
768     return m_url == newUrl;
769 }
770 
loadGeneratorLibrary(const KPluginMetaData & service)771 Generator *DocumentPrivate::loadGeneratorLibrary(const KPluginMetaData &service)
772 {
773     KPluginLoader loader(service.fileName());
774     qCDebug(OkularCoreDebug) << service.fileName();
775     KPluginFactory *factory = loader.factory();
776     if (!factory) {
777         qCWarning(OkularCoreDebug).nospace() << "Invalid plugin factory for " << service.fileName() << ":" << loader.errorString();
778         return nullptr;
779     }
780 
781     Generator *plugin = factory->create<Okular::Generator>();
782 
783     GeneratorInfo info(plugin, service);
784     m_loadedGenerators.insert(service.pluginId(), info);
785     return plugin;
786 }
787 
loadAllGeneratorLibraries()788 void DocumentPrivate::loadAllGeneratorLibraries()
789 {
790     if (m_generatorsLoaded)
791         return;
792 
793     loadServiceList(availableGenerators());
794 
795     m_generatorsLoaded = true;
796 }
797 
loadServiceList(const QVector<KPluginMetaData> & offers)798 void DocumentPrivate::loadServiceList(const QVector<KPluginMetaData> &offers)
799 {
800     int count = offers.count();
801     if (count <= 0)
802         return;
803 
804     for (int i = 0; i < count; ++i) {
805         QString id = offers.at(i).pluginId();
806         // don't load already loaded generators
807         QHash<QString, GeneratorInfo>::const_iterator genIt = m_loadedGenerators.constFind(id);
808         if (!m_loadedGenerators.isEmpty() && genIt != m_loadedGenerators.constEnd())
809             continue;
810 
811         Generator *g = loadGeneratorLibrary(offers.at(i));
812         (void)g;
813     }
814 }
815 
unloadGenerator(const GeneratorInfo & info)816 void DocumentPrivate::unloadGenerator(const GeneratorInfo &info)
817 {
818     delete info.generator;
819 }
820 
cacheExportFormats()821 void DocumentPrivate::cacheExportFormats()
822 {
823     if (m_exportCached)
824         return;
825 
826     const ExportFormat::List formats = m_generator->exportFormats();
827     for (int i = 0; i < formats.count(); ++i) {
828         if (formats.at(i).mimeType().name() == QLatin1String("text/plain"))
829             m_exportToText = formats.at(i);
830         else
831             m_exportFormats.append(formats.at(i));
832     }
833 
834     m_exportCached = true;
835 }
836 
generatorConfig(GeneratorInfo & info)837 ConfigInterface *DocumentPrivate::generatorConfig(GeneratorInfo &info)
838 {
839     if (info.configChecked)
840         return info.config;
841 
842     info.config = qobject_cast<Okular::ConfigInterface *>(info.generator);
843     info.configChecked = true;
844     return info.config;
845 }
846 
generatorSave(GeneratorInfo & info)847 SaveInterface *DocumentPrivate::generatorSave(GeneratorInfo &info)
848 {
849     if (info.saveChecked)
850         return info.save;
851 
852     info.save = qobject_cast<Okular::SaveInterface *>(info.generator);
853     info.saveChecked = true;
854     return info.save;
855 }
856 
openDocumentInternal(const KPluginMetaData & offer,bool isstdin,const QString & docFile,const QByteArray & filedata,const QString & password)857 Document::OpenResult DocumentPrivate::openDocumentInternal(const KPluginMetaData &offer, bool isstdin, const QString &docFile, const QByteArray &filedata, const QString &password)
858 {
859     QString propName = offer.pluginId();
860     QHash<QString, GeneratorInfo>::const_iterator genIt = m_loadedGenerators.constFind(propName);
861     m_walletGenerator = nullptr;
862     if (genIt != m_loadedGenerators.constEnd()) {
863         m_generator = genIt.value().generator;
864     } else {
865         m_generator = loadGeneratorLibrary(offer);
866         if (!m_generator)
867             return Document::OpenError;
868         genIt = m_loadedGenerators.constFind(propName);
869         Q_ASSERT(genIt != m_loadedGenerators.constEnd());
870     }
871     Q_ASSERT_X(m_generator, "Document::load()", "null generator?!");
872 
873     m_generator->d_func()->m_document = this;
874 
875     // connect error reporting signals
876     m_openError.clear();
877     QMetaObject::Connection errorToOpenErrorConnection = QObject::connect(m_generator, &Generator::error, m_parent, [this](const QString &message) { m_openError = message; });
878     QObject::connect(m_generator, &Generator::warning, m_parent, &Document::warning);
879     QObject::connect(m_generator, &Generator::notice, m_parent, &Document::notice);
880 
881     QApplication::setOverrideCursor(Qt::WaitCursor);
882 
883     const QSizeF dpi = Utils::realDpi(m_widget);
884     qCDebug(OkularCoreDebug) << "Output DPI:" << dpi;
885     m_generator->setDPI(dpi);
886 
887     Document::OpenResult openResult = Document::OpenError;
888     if (!isstdin) {
889         openResult = m_generator->loadDocumentWithPassword(docFile, m_pagesVector, password);
890     } else if (!filedata.isEmpty()) {
891         if (m_generator->hasFeature(Generator::ReadRawData)) {
892             openResult = m_generator->loadDocumentFromDataWithPassword(filedata, m_pagesVector, password);
893         } else {
894             m_tempFile = new QTemporaryFile();
895             if (!m_tempFile->open()) {
896                 delete m_tempFile;
897                 m_tempFile = nullptr;
898             } else {
899                 m_tempFile->write(filedata);
900                 QString tmpFileName = m_tempFile->fileName();
901                 m_tempFile->close();
902                 openResult = m_generator->loadDocumentWithPassword(tmpFileName, m_pagesVector, password);
903             }
904         }
905     }
906 
907     QApplication::restoreOverrideCursor();
908     if (openResult != Document::OpenSuccess || m_pagesVector.size() <= 0) {
909         m_generator->d_func()->m_document = nullptr;
910         QObject::disconnect(m_generator, nullptr, m_parent, nullptr);
911 
912         // TODO this is a bit of a hack, since basically means that
913         // you can only call walletDataForFile after calling openDocument
914         // but since in reality it's what happens I've decided not to refactor/break API
915         // One solution is just kill walletDataForFile and make OpenResult be an object
916         // where the wallet data is also returned when OpenNeedsPassword
917         m_walletGenerator = m_generator;
918         m_generator = nullptr;
919 
920         qDeleteAll(m_pagesVector);
921         m_pagesVector.clear();
922         delete m_tempFile;
923         m_tempFile = nullptr;
924 
925         // TODO: emit a message telling the document is empty
926         if (openResult == Document::OpenSuccess)
927             openResult = Document::OpenError;
928     } else {
929         /*
930          *  Now that the documen is opened, the tab (if using tabs) is visible, which mean that
931          * we can now connect the error reporting signal directly to the parent
932          */
933 
934         QObject::disconnect(errorToOpenErrorConnection);
935         QObject::connect(m_generator, &Generator::error, m_parent, &Document::error);
936     }
937 
938     return openResult;
939 }
940 
savePageDocumentInfo(QTemporaryFile * infoFile,int what) const941 bool DocumentPrivate::savePageDocumentInfo(QTemporaryFile *infoFile, int what) const
942 {
943     if (infoFile->open()) {
944         // 1. Create DOM
945         QDomDocument doc(QStringLiteral("documentInfo"));
946         QDomProcessingInstruction xmlPi = doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
947         doc.appendChild(xmlPi);
948         QDomElement root = doc.createElement(QStringLiteral("documentInfo"));
949         doc.appendChild(root);
950 
951         // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM
952         QDomElement pageList = doc.createElement(QStringLiteral("pageList"));
953         root.appendChild(pageList);
954         // <page list><page number='x'>.... </page> save pages that hold data
955         QVector<Page *>::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd();
956         for (; pIt != pEnd; ++pIt)
957             (*pIt)->d->saveLocalContents(pageList, doc, PageItems(what));
958 
959         // 3. Save DOM to XML file
960         QString xml = doc.toString();
961         QTextStream os(infoFile);
962         os.setCodec("UTF-8");
963         os << xml;
964         return true;
965     }
966     return false;
967 }
968 
nextDocumentViewport() const969 DocumentViewport DocumentPrivate::nextDocumentViewport() const
970 {
971     DocumentViewport ret = m_nextDocumentViewport;
972     if (!m_nextDocumentDestination.isEmpty() && m_generator) {
973         DocumentViewport vp(m_parent->metaData(QStringLiteral("NamedViewport"), m_nextDocumentDestination).toString());
974         if (vp.isValid()) {
975             ret = vp;
976         }
977     }
978     return ret;
979 }
980 
performAddPageAnnotation(int page,Annotation * annotation)981 void DocumentPrivate::performAddPageAnnotation(int page, Annotation *annotation)
982 {
983     Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
984     AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr;
985 
986     // find out the page to attach annotation
987     Page *kp = m_pagesVector[page];
988     if (!m_generator || !kp)
989         return;
990 
991     // the annotation belongs already to a page
992     if (annotation->d_ptr->m_page)
993         return;
994 
995     // add annotation to the page
996     kp->addAnnotation(annotation);
997 
998     // tell the annotation proxy
999     if (proxy && proxy->supports(AnnotationProxy::Addition))
1000         proxy->notifyAddition(annotation, page);
1001 
1002     // notify observers about the change
1003     notifyAnnotationChanges(page);
1004 
1005     if (annotation->flags() & Annotation::ExternallyDrawn) {
1006         // Redraw everything, including ExternallyDrawn annotations
1007         refreshPixmaps(page);
1008     }
1009 }
1010 
performRemovePageAnnotation(int page,Annotation * annotation)1011 void DocumentPrivate::performRemovePageAnnotation(int page, Annotation *annotation)
1012 {
1013     Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
1014     AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr;
1015     bool isExternallyDrawn;
1016 
1017     // find out the page
1018     Page *kp = m_pagesVector[page];
1019     if (!m_generator || !kp)
1020         return;
1021 
1022     if (annotation->flags() & Annotation::ExternallyDrawn)
1023         isExternallyDrawn = true;
1024     else
1025         isExternallyDrawn = false;
1026 
1027     // try to remove the annotation
1028     if (m_parent->canRemovePageAnnotation(annotation)) {
1029         // tell the annotation proxy
1030         if (proxy && proxy->supports(AnnotationProxy::Removal))
1031             proxy->notifyRemoval(annotation, page);
1032 
1033         kp->removeAnnotation(annotation); // Also destroys the object
1034 
1035         // in case of success, notify observers about the change
1036         notifyAnnotationChanges(page);
1037 
1038         if (isExternallyDrawn) {
1039             // Redraw everything, including ExternallyDrawn annotations
1040             refreshPixmaps(page);
1041         }
1042     }
1043 }
1044 
performModifyPageAnnotation(int page,Annotation * annotation,bool appearanceChanged)1045 void DocumentPrivate::performModifyPageAnnotation(int page, Annotation *annotation, bool appearanceChanged)
1046 {
1047     Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
1048     AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr;
1049 
1050     // find out the page
1051     Page *kp = m_pagesVector[page];
1052     if (!m_generator || !kp)
1053         return;
1054 
1055     // tell the annotation proxy
1056     if (proxy && proxy->supports(AnnotationProxy::Modification)) {
1057         proxy->notifyModification(annotation, page, appearanceChanged);
1058     }
1059 
1060     // notify observers about the change
1061     notifyAnnotationChanges(page);
1062     if (appearanceChanged && (annotation->flags() & Annotation::ExternallyDrawn)) {
1063         /* When an annotation is being moved, the generator will not render it.
1064          * Therefore there's no need to refresh pixmaps after the first time */
1065         if (annotation->flags() & (Annotation::BeingMoved | Annotation::BeingResized)) {
1066             if (m_annotationBeingModified)
1067                 return;
1068             else // First time: take note
1069                 m_annotationBeingModified = true;
1070         } else {
1071             m_annotationBeingModified = false;
1072         }
1073 
1074         // Redraw everything, including ExternallyDrawn annotations
1075         qCDebug(OkularCoreDebug) << "Refreshing Pixmaps";
1076         refreshPixmaps(page);
1077     }
1078 }
1079 
performSetAnnotationContents(const QString & newContents,Annotation * annot,int pageNumber)1080 void DocumentPrivate::performSetAnnotationContents(const QString &newContents, Annotation *annot, int pageNumber)
1081 {
1082     bool appearanceChanged = false;
1083 
1084     // Check if appearanceChanged should be true
1085     switch (annot->subType()) {
1086     // If it's an in-place TextAnnotation, set the inplace text
1087     case Okular::Annotation::AText: {
1088         Okular::TextAnnotation *txtann = static_cast<Okular::TextAnnotation *>(annot);
1089         if (txtann->textType() == Okular::TextAnnotation::InPlace) {
1090             appearanceChanged = true;
1091         }
1092         break;
1093     }
1094     // If it's a LineAnnotation, check if caption text is visible
1095     case Okular::Annotation::ALine: {
1096         Okular::LineAnnotation *lineann = static_cast<Okular::LineAnnotation *>(annot);
1097         if (lineann->showCaption())
1098             appearanceChanged = true;
1099         break;
1100     }
1101     default:
1102         break;
1103     }
1104 
1105     // Set contents
1106     annot->setContents(newContents);
1107 
1108     // Tell the document the annotation has been modified
1109     performModifyPageAnnotation(pageNumber, annot, appearanceChanged);
1110 }
1111 
recalculateForms()1112 void DocumentPrivate::recalculateForms()
1113 {
1114     const QVariant fco = m_parent->metaData(QStringLiteral("FormCalculateOrder"));
1115     const QVector<int> formCalculateOrder = fco.value<QVector<int>>();
1116     foreach (int formId, formCalculateOrder) {
1117         for (uint pageIdx = 0; pageIdx < m_parent->pages(); pageIdx++) {
1118             const Page *p = m_parent->page(pageIdx);
1119             if (p) {
1120                 bool pageNeedsRefresh = false;
1121                 foreach (FormField *form, p->formFields()) {
1122                     if (form->id() == formId) {
1123                         Action *action = form->additionalAction(FormField::CalculateField);
1124                         if (action) {
1125                             FormFieldText *fft = dynamic_cast<FormFieldText *>(form);
1126                             std::shared_ptr<Event> event;
1127                             QString oldVal;
1128                             if (fft) {
1129                                 // Prepare text calculate event
1130                                 event = Event::createFormCalculateEvent(fft, m_pagesVector[pageIdx]);
1131                                 if (!m_scripter)
1132                                     m_scripter = new Scripter(this);
1133                                 m_scripter->setEvent(event.get());
1134                                 // The value maybe changed in javascript so save it first.
1135                                 oldVal = fft->text();
1136                             }
1137 
1138                             m_parent->processAction(action);
1139                             if (event && fft) {
1140                                 // Update text field from calculate
1141                                 m_scripter->setEvent(nullptr);
1142                                 const QString newVal = event->value().toString();
1143                                 if (newVal != oldVal) {
1144                                     fft->setText(newVal);
1145                                     fft->setAppearanceText(newVal);
1146                                     if (const Okular::Action *action = fft->additionalAction(Okular::FormField::FormatField)) {
1147                                         // The format action handles the refresh.
1148                                         m_parent->processFormatAction(action, fft);
1149                                     } else {
1150                                         emit m_parent->refreshFormWidget(fft);
1151                                         pageNeedsRefresh = true;
1152                                     }
1153                                 }
1154                             }
1155                         } else {
1156                             qWarning() << "Form that is part of calculate order doesn't have a calculate action";
1157                         }
1158                     }
1159                 }
1160                 if (pageNeedsRefresh) {
1161                     refreshPixmaps(p->number());
1162                 }
1163             }
1164         }
1165     }
1166 }
1167 
saveDocumentInfo() const1168 void DocumentPrivate::saveDocumentInfo() const
1169 {
1170     if (m_xmlFileName.isEmpty())
1171         return;
1172 
1173     QFile infoFile(m_xmlFileName);
1174     qCDebug(OkularCoreDebug) << "About to save document info to" << m_xmlFileName;
1175     if (!infoFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
1176         qCWarning(OkularCoreDebug) << "Failed to open docdata file" << m_xmlFileName;
1177         return;
1178     }
1179     // 1. Create DOM
1180     QDomDocument doc(QStringLiteral("documentInfo"));
1181     QDomProcessingInstruction xmlPi = doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
1182     doc.appendChild(xmlPi);
1183     QDomElement root = doc.createElement(QStringLiteral("documentInfo"));
1184     root.setAttribute(QStringLiteral("url"), m_url.toDisplayString(QUrl::PreferLocalFile));
1185     doc.appendChild(root);
1186 
1187     // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM
1188     //  -> do this if there are not-yet-migrated annots or forms in docdata/
1189     if (m_docdataMigrationNeeded) {
1190         QDomElement pageList = doc.createElement(QStringLiteral("pageList"));
1191         root.appendChild(pageList);
1192         // OriginalAnnotationPageItems and OriginalFormFieldPageItems tell to
1193         // store the same unmodified annotation list and form contents that we
1194         // read when we opened the file and ignore any change made by the user.
1195         // Since we don't store annotations and forms in docdata/ any more, this is
1196         // necessary to preserve annotations/forms that previous Okular version
1197         // had stored there.
1198         const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems | OriginalFormFieldPageItems;
1199         // <page list><page number='x'>.... </page> save pages that hold data
1200         QVector<Page *>::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd();
1201         for (; pIt != pEnd; ++pIt)
1202             (*pIt)->d->saveLocalContents(pageList, doc, saveWhat);
1203     }
1204 
1205     // 2.2. Save document info (current viewport, history, ... ) to DOM
1206     QDomElement generalInfo = doc.createElement(QStringLiteral("generalInfo"));
1207     root.appendChild(generalInfo);
1208     // create rotation node
1209     if (m_rotation != Rotation0) {
1210         QDomElement rotationNode = doc.createElement(QStringLiteral("rotation"));
1211         generalInfo.appendChild(rotationNode);
1212         rotationNode.appendChild(doc.createTextNode(QString::number((int)m_rotation)));
1213     }
1214     // <general info><history> ... </history> save history up to OKULAR_HISTORY_SAVEDSTEPS viewports
1215     const auto currentViewportIterator = QLinkedList<DocumentViewport>::const_iterator(m_viewportIterator);
1216     QLinkedList<DocumentViewport>::const_iterator backIterator = currentViewportIterator;
1217     if (backIterator != m_viewportHistory.constEnd()) {
1218         // go back up to OKULAR_HISTORY_SAVEDSTEPS steps from the current viewportIterator
1219         int backSteps = OKULAR_HISTORY_SAVEDSTEPS;
1220         while (backSteps-- && backIterator != m_viewportHistory.constBegin())
1221             --backIterator;
1222 
1223         // create history root node
1224         QDomElement historyNode = doc.createElement(QStringLiteral("history"));
1225         generalInfo.appendChild(historyNode);
1226 
1227         // add old[backIterator] and present[viewportIterator] items
1228         QLinkedList<DocumentViewport>::const_iterator endIt = currentViewportIterator;
1229         ++endIt;
1230         while (backIterator != endIt) {
1231             QString name = (backIterator == currentViewportIterator) ? QStringLiteral("current") : QStringLiteral("oldPage");
1232             QDomElement historyEntry = doc.createElement(name);
1233             historyEntry.setAttribute(QStringLiteral("viewport"), (*backIterator).toString());
1234             historyNode.appendChild(historyEntry);
1235             ++backIterator;
1236         }
1237     }
1238     // create views root node
1239     QDomElement viewsNode = doc.createElement(QStringLiteral("views"));
1240     generalInfo.appendChild(viewsNode);
1241     for (View *view : qAsConst(m_views)) {
1242         QDomElement viewEntry = doc.createElement(QStringLiteral("view"));
1243         viewEntry.setAttribute(QStringLiteral("name"), view->name());
1244         viewsNode.appendChild(viewEntry);
1245         saveViewsInfo(view, viewEntry);
1246     }
1247 
1248     // 3. Save DOM to XML file
1249     QString xml = doc.toString();
1250     QTextStream os(&infoFile);
1251     os.setCodec("UTF-8");
1252     os << xml;
1253     infoFile.close();
1254 }
1255 
slotTimedMemoryCheck()1256 void DocumentPrivate::slotTimedMemoryCheck()
1257 {
1258     // [MEM] clean memory (for 'free mem dependent' profiles only)
1259     if (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Low && m_allocatedPixmapsTotalMemory > 1024 * 1024)
1260         cleanupPixmapMemory();
1261 }
1262 
sendGeneratorPixmapRequest()1263 void DocumentPrivate::sendGeneratorPixmapRequest()
1264 {
1265     /* If the pixmap cache will have to be cleaned in order to make room for the
1266      * next request, get the distance from the current viewport of the page
1267      * whose pixmap will be removed. We will ignore preload requests for pages
1268      * that are at the same distance or farther */
1269     const qulonglong memoryToFree = calculateMemoryToFree();
1270     const int currentViewportPage = (*m_viewportIterator).pageNumber;
1271     int maxDistance = INT_MAX; // Default: No maximum
1272     if (memoryToFree) {
1273         AllocatedPixmap *pixmapToReplace = searchLowestPriorityPixmap(true);
1274         if (pixmapToReplace)
1275             maxDistance = qAbs(pixmapToReplace->page - currentViewportPage);
1276     }
1277 
1278     // find a request
1279     PixmapRequest *request = nullptr;
1280     m_pixmapRequestsMutex.lock();
1281     while (!m_pixmapRequestsStack.isEmpty() && !request) {
1282         PixmapRequest *r = m_pixmapRequestsStack.last();
1283         if (!r) {
1284             m_pixmapRequestsStack.pop_back();
1285             continue;
1286         }
1287 
1288         QRect requestRect = r->isTile() ? r->normalizedRect().geometry(r->width(), r->height()) : QRect(0, 0, r->width(), r->height());
1289         TilesManager *tilesManager = r->d->tilesManager();
1290         const double normalizedArea = r->normalizedRect().width() * r->normalizedRect().height();
1291         const QScreen *screen = nullptr;
1292         if (m_widget) {
1293             const QWindow *window = m_widget->window()->windowHandle();
1294             if (window)
1295                 screen = window->screen();
1296         }
1297         if (!screen)
1298             screen = QGuiApplication::primaryScreen();
1299         const long screenSize = screen->devicePixelRatio() * screen->size().width() * screen->devicePixelRatio() * screen->size().height();
1300 
1301         // If it's a preload but the generator is not threaded no point in trying to preload
1302         if (r->preload() && !m_generator->hasFeature(Generator::Threaded)) {
1303             m_pixmapRequestsStack.pop_back();
1304             delete r;
1305         }
1306         // request only if page isn't already present and request has valid id
1307         else if ((!r->d->mForce && r->page()->hasPixmap(r->observer(), r->width(), r->height(), r->normalizedRect())) || !m_observers.contains(r->observer())) {
1308             m_pixmapRequestsStack.pop_back();
1309             delete r;
1310         } else if (!r->d->mForce && r->preload() && qAbs(r->pageNumber() - currentViewportPage) >= maxDistance) {
1311             m_pixmapRequestsStack.pop_back();
1312             // qCDebug(OkularCoreDebug) << "Ignoring request that doesn't fit in cache";
1313             delete r;
1314         }
1315         // Ignore requests for pixmaps that are already being generated
1316         else if (tilesManager && tilesManager->isRequesting(r->normalizedRect(), r->width(), r->height())) {
1317             m_pixmapRequestsStack.pop_back();
1318             delete r;
1319         }
1320         // If the requested area is above 4*screenSize pixels, and we're not rendering most of the page,  switch on the tile manager
1321         else if (!tilesManager && m_generator->hasFeature(Generator::TiledRendering) && (long)r->width() * (long)r->height() > 4L * screenSize && normalizedArea < 0.75 && normalizedArea != 0) {
1322             // if the image is too big. start using tiles
1323             qCDebug(OkularCoreDebug).nospace() << "Start using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1324 
1325             // fill the tiles manager with the last rendered pixmap
1326             const QPixmap *pixmap = r->page()->_o_nearestPixmap(r->observer(), r->width(), r->height());
1327             if (pixmap) {
1328                 tilesManager = new TilesManager(r->pageNumber(), pixmap->width(), pixmap->height(), r->page()->rotation());
1329                 tilesManager->setPixmap(pixmap, NormalizedRect(0, 0, 1, 1), true /*isPartialPixmap*/);
1330                 tilesManager->setSize(r->width(), r->height());
1331             } else {
1332                 // create new tiles manager
1333                 tilesManager = new TilesManager(r->pageNumber(), r->width(), r->height(), r->page()->rotation());
1334             }
1335             tilesManager->setRequest(r->normalizedRect(), r->width(), r->height());
1336             r->page()->deletePixmap(r->observer());
1337             r->page()->d->setTilesManager(r->observer(), tilesManager);
1338             r->setTile(true);
1339 
1340             // Change normalizedRect to the smallest rect that contains all
1341             // visible tiles.
1342             if (!r->normalizedRect().isNull()) {
1343                 NormalizedRect tilesRect;
1344                 const QList<Tile> tiles = tilesManager->tilesAt(r->normalizedRect(), TilesManager::TerminalTile);
1345                 QList<Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
1346                 while (tIt != tEnd) {
1347                     Tile tile = *tIt;
1348                     if (tilesRect.isNull())
1349                         tilesRect = tile.rect();
1350                     else
1351                         tilesRect |= tile.rect();
1352 
1353                     ++tIt;
1354                 }
1355 
1356                 r->setNormalizedRect(tilesRect);
1357                 request = r;
1358             } else {
1359                 // Discard request if normalizedRect is null. This happens in
1360                 // preload requests issued by PageView if the requested page is
1361                 // not visible and the user has just switched from a non-tiled
1362                 // zoom level to a tiled one
1363                 m_pixmapRequestsStack.pop_back();
1364                 delete r;
1365             }
1366         }
1367         // If the requested area is below 3*screenSize pixels, switch off the tile manager
1368         else if (tilesManager && (long)r->width() * (long)r->height() < 3L * screenSize) {
1369             qCDebug(OkularCoreDebug).nospace() << "Stop using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1370 
1371             // page is too small. stop using tiles.
1372             r->page()->deletePixmap(r->observer());
1373             r->setTile(false);
1374 
1375             request = r;
1376         } else if ((long)requestRect.width() * (long)requestRect.height() > 100L * screenSize && (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Greedy)) {
1377             m_pixmapRequestsStack.pop_back();
1378             if (!m_warnedOutOfMemory) {
1379                 qCWarning(OkularCoreDebug).nospace() << "Running out of memory on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1380                 qCWarning(OkularCoreDebug) << "this message will be reported only once.";
1381                 m_warnedOutOfMemory = true;
1382             }
1383             delete r;
1384         } else {
1385             request = r;
1386         }
1387     }
1388 
1389     // if no request found (or already generated), return
1390     if (!request) {
1391         m_pixmapRequestsMutex.unlock();
1392         return;
1393     }
1394 
1395     // [MEM] preventive memory freeing
1396     qulonglong pixmapBytes = 0;
1397     TilesManager *tm = request->d->tilesManager();
1398     if (tm)
1399         pixmapBytes = tm->totalMemory();
1400     else
1401         pixmapBytes = 4 * request->width() * request->height();
1402 
1403     if (pixmapBytes > (1024 * 1024))
1404         cleanupPixmapMemory(memoryToFree /* previously calculated value */);
1405 
1406     // submit the request to the generator
1407     if (m_generator->canGeneratePixmap()) {
1408         QRect requestRect = !request->isTile() ? QRect(0, 0, request->width(), request->height()) : request->normalizedRect().geometry(request->width(), request->height());
1409         qCDebug(OkularCoreDebug).nospace() << "sending request observer=" << request->observer() << " " << requestRect.width() << "x" << requestRect.height() << "@" << request->pageNumber() << " async == " << request->asynchronous()
1410                                            << " isTile == " << request->isTile();
1411         m_pixmapRequestsStack.removeAll(request);
1412 
1413         if (tm)
1414             tm->setRequest(request->normalizedRect(), request->width(), request->height());
1415 
1416         if ((int)m_rotation % 2)
1417             request->d->swap();
1418 
1419         if (m_rotation != Rotation0 && !request->normalizedRect().isNull())
1420             request->setNormalizedRect(TilesManager::fromRotatedRect(request->normalizedRect(), m_rotation));
1421 
1422         // If set elsewhere we already know we want it to be partial
1423         if (!request->partialUpdatesWanted()) {
1424             request->setPartialUpdatesWanted(request->asynchronous() && !request->page()->hasPixmap(request->observer()));
1425         }
1426 
1427         // we always have to unlock _before_ the generatePixmap() because
1428         // a sync generation would end with requestDone() -> deadlock, and
1429         // we can not really know if the generator can do async requests
1430         m_executingPixmapRequests.push_back(request);
1431         m_pixmapRequestsMutex.unlock();
1432         m_generator->generatePixmap(request);
1433     } else {
1434         m_pixmapRequestsMutex.unlock();
1435         // pino (7/4/2006): set the polling interval from 10 to 30
1436         QTimer::singleShot(30, m_parent, [this] { sendGeneratorPixmapRequest(); });
1437     }
1438 }
1439 
rotationFinished(int page,Okular::Page * okularPage)1440 void DocumentPrivate::rotationFinished(int page, Okular::Page *okularPage)
1441 {
1442     Okular::Page *wantedPage = m_pagesVector.value(page, nullptr);
1443     if (!wantedPage || wantedPage != okularPage)
1444         return;
1445 
1446     foreach (DocumentObserver *o, m_observers)
1447         o->notifyPageChanged(page, DocumentObserver::Pixmap | DocumentObserver::Annotations);
1448 }
1449 
slotFontReadingProgress(int page)1450 void DocumentPrivate::slotFontReadingProgress(int page)
1451 {
1452     emit m_parent->fontReadingProgress(page);
1453 
1454     if (page >= (int)m_parent->pages() - 1) {
1455         emit m_parent->fontReadingEnded();
1456         m_fontThread = nullptr;
1457         m_fontsCached = true;
1458     }
1459 }
1460 
fontReadingGotFont(const Okular::FontInfo & font)1461 void DocumentPrivate::fontReadingGotFont(const Okular::FontInfo &font)
1462 {
1463     // Try to avoid duplicate fonts
1464     if (m_fontsCache.indexOf(font) == -1) {
1465         m_fontsCache.append(font);
1466 
1467         emit m_parent->gotFont(font);
1468     }
1469 }
1470 
slotGeneratorConfigChanged()1471 void DocumentPrivate::slotGeneratorConfigChanged()
1472 {
1473     if (!m_generator)
1474         return;
1475 
1476     // reparse generator config and if something changed clear Pages
1477     bool configchanged = false;
1478     QHash<QString, GeneratorInfo>::iterator it = m_loadedGenerators.begin(), itEnd = m_loadedGenerators.end();
1479     for (; it != itEnd; ++it) {
1480         Okular::ConfigInterface *iface = generatorConfig(it.value());
1481         if (iface) {
1482             bool it_changed = iface->reparseConfig();
1483             if (it_changed && (m_generator == it.value().generator))
1484                 configchanged = true;
1485         }
1486     }
1487     if (configchanged) {
1488         // invalidate pixmaps
1489         QVector<Page *>::const_iterator it = m_pagesVector.constBegin(), end = m_pagesVector.constEnd();
1490         for (; it != end; ++it) {
1491             (*it)->deletePixmaps();
1492         }
1493 
1494         // [MEM] remove allocation descriptors
1495         qDeleteAll(m_allocatedPixmaps);
1496         m_allocatedPixmaps.clear();
1497         m_allocatedPixmapsTotalMemory = 0;
1498 
1499         // send reload signals to observers
1500         foreachObserverD(notifyContentsCleared(DocumentObserver::Pixmap));
1501     }
1502 
1503     // free memory if in 'low' profile
1504     if (SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !m_allocatedPixmaps.isEmpty() && !m_pagesVector.isEmpty())
1505         cleanupPixmapMemory();
1506 }
1507 
refreshPixmaps(int pageNumber)1508 void DocumentPrivate::refreshPixmaps(int pageNumber)
1509 {
1510     Page *page = m_pagesVector.value(pageNumber, nullptr);
1511     if (!page)
1512         return;
1513 
1514     QMap<DocumentObserver *, PagePrivate::PixmapObject>::ConstIterator it = page->d->m_pixmaps.constBegin(), itEnd = page->d->m_pixmaps.constEnd();
1515     QVector<Okular::PixmapRequest *> pixmapsToRequest;
1516     for (; it != itEnd; ++it) {
1517         const QSize size = (*it).m_pixmap->size();
1518         PixmapRequest *p = new PixmapRequest(it.key(), pageNumber, size.width(), size.height(), 1 /* dpr */, 1, PixmapRequest::Asynchronous);
1519         p->d->mForce = true;
1520         pixmapsToRequest << p;
1521     }
1522 
1523     // Need to do this ↑↓ in two steps since requestPixmaps can end up calling cancelRenderingBecauseOf
1524     // which changes m_pixmaps and thus breaks the loop above
1525     for (PixmapRequest *pr : qAsConst(pixmapsToRequest)) {
1526         QLinkedList<Okular::PixmapRequest *> requestedPixmaps;
1527         requestedPixmaps.push_back(pr);
1528         m_parent->requestPixmaps(requestedPixmaps, Okular::Document::NoOption);
1529     }
1530 
1531     for (DocumentObserver *observer : qAsConst(m_observers)) {
1532         QLinkedList<Okular::PixmapRequest *> requestedPixmaps;
1533 
1534         TilesManager *tilesManager = page->d->tilesManager(observer);
1535         if (tilesManager) {
1536             tilesManager->markDirty();
1537 
1538             PixmapRequest *p = new PixmapRequest(observer, pageNumber, tilesManager->width(), tilesManager->height(), 1 /* dpr */, 1, PixmapRequest::Asynchronous);
1539 
1540             // Get the visible page rect
1541             NormalizedRect visibleRect;
1542             QVector<Okular::VisiblePageRect *>::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd();
1543             for (; vIt != vEnd; ++vIt) {
1544                 if ((*vIt)->pageNumber == pageNumber) {
1545                     visibleRect = (*vIt)->rect;
1546                     break;
1547                 }
1548             }
1549 
1550             if (!visibleRect.isNull()) {
1551                 p->setNormalizedRect(visibleRect);
1552                 p->setTile(true);
1553                 p->d->mForce = true;
1554                 requestedPixmaps.push_back(p);
1555             } else {
1556                 delete p;
1557             }
1558         }
1559 
1560         m_parent->requestPixmaps(requestedPixmaps, Okular::Document::NoOption);
1561     }
1562 }
1563 
_o_configChanged()1564 void DocumentPrivate::_o_configChanged()
1565 {
1566     // free text pages if needed
1567     calculateMaxTextPages();
1568     while (m_allocatedTextPagesFifo.count() > m_maxAllocatedTextPages) {
1569         int pageToKick = m_allocatedTextPagesFifo.takeFirst();
1570         m_pagesVector.at(pageToKick)->setTextPage(nullptr); // deletes the textpage
1571     }
1572 }
1573 
doContinueDirectionMatchSearch(void * doContinueDirectionMatchSearchStruct)1574 void DocumentPrivate::doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct)
1575 {
1576     DoContinueDirectionMatchSearchStruct *searchStruct = static_cast<DoContinueDirectionMatchSearchStruct *>(doContinueDirectionMatchSearchStruct);
1577     RunningSearch *search = m_searches.value(searchStruct->searchID);
1578 
1579     if ((m_searchCancelled && !searchStruct->match) || !search) {
1580         // if the user cancelled but he just got a match, give him the match!
1581         QApplication::restoreOverrideCursor();
1582 
1583         if (search)
1584             search->isCurrentlySearching = false;
1585 
1586         emit m_parent->searchFinished(searchStruct->searchID, Document::SearchCancelled);
1587         delete searchStruct->pagesToNotify;
1588         delete searchStruct;
1589         return;
1590     }
1591 
1592     const bool forward = search->cachedType == Document::NextMatch;
1593     bool doContinue = false;
1594     // if no match found, loop through the whole doc, starting from currentPage
1595     if (!searchStruct->match) {
1596         const int pageCount = m_pagesVector.count();
1597         if (search->pagesDone < pageCount) {
1598             doContinue = true;
1599             if (searchStruct->currentPage >= pageCount) {
1600                 searchStruct->currentPage = 0;
1601                 emit m_parent->notice(i18n("Continuing search from beginning"), 3000);
1602             } else if (searchStruct->currentPage < 0) {
1603                 searchStruct->currentPage = pageCount - 1;
1604                 emit m_parent->notice(i18n("Continuing search from bottom"), 3000);
1605             }
1606         }
1607     }
1608 
1609     if (doContinue) {
1610         // get page
1611         Page *page = m_pagesVector[searchStruct->currentPage];
1612         // request search page if needed
1613         if (!page->hasTextPage())
1614             m_parent->requestTextPage(page->number());
1615 
1616         // if found a match on the current page, end the loop
1617         searchStruct->match = page->findText(searchStruct->searchID, search->cachedString, forward ? FromTop : FromBottom, search->cachedCaseSensitivity);
1618         if (!searchStruct->match) {
1619             if (forward)
1620                 searchStruct->currentPage++;
1621             else
1622                 searchStruct->currentPage--;
1623             search->pagesDone++;
1624         } else {
1625             search->pagesDone = 1;
1626         }
1627 
1628         // Both of the previous if branches need to call doContinueDirectionMatchSearch
1629         QTimer::singleShot(0, m_parent, [this, searchStruct] { doContinueDirectionMatchSearch(searchStruct); });
1630     } else {
1631         doProcessSearchMatch(searchStruct->match, search, searchStruct->pagesToNotify, searchStruct->currentPage, searchStruct->searchID, search->cachedViewportMove, search->cachedColor);
1632         delete searchStruct;
1633     }
1634 }
1635 
doProcessSearchMatch(RegularAreaRect * match,RunningSearch * search,QSet<int> * pagesToNotify,int currentPage,int searchID,bool moveViewport,const QColor & color)1636 void DocumentPrivate::doProcessSearchMatch(RegularAreaRect *match, RunningSearch *search, QSet<int> *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor &color)
1637 {
1638     // reset cursor to previous shape
1639     QApplication::restoreOverrideCursor();
1640 
1641     bool foundAMatch = false;
1642 
1643     search->isCurrentlySearching = false;
1644 
1645     // if a match has been found..
1646     if (match) {
1647         // update the RunningSearch structure adding this match..
1648         foundAMatch = true;
1649         search->continueOnPage = currentPage;
1650         search->continueOnMatch = *match;
1651         search->highlightedPages.insert(currentPage);
1652         // ..add highlight to the page..
1653         m_pagesVector[currentPage]->d->setHighlight(searchID, match, color);
1654 
1655         // ..queue page for notifying changes..
1656         pagesToNotify->insert(currentPage);
1657 
1658         // Create a normalized rectangle around the search match that includes a 5% buffer on all sides.
1659         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);
1660 
1661         const bool matchRectFullyVisible = isNormalizedRectangleFullyVisible(matchRectWithBuffer, currentPage);
1662 
1663         // ..move the viewport to show the first of the searched word sequence centered
1664         if (moveViewport && !matchRectFullyVisible) {
1665             DocumentViewport searchViewport(currentPage);
1666             searchViewport.rePos.enabled = true;
1667             searchViewport.rePos.normalizedX = (match->first().left + match->first().right) / 2.0;
1668             searchViewport.rePos.normalizedY = (match->first().top + match->first().bottom) / 2.0;
1669             m_parent->setViewport(searchViewport, nullptr, true);
1670         }
1671         delete match;
1672     }
1673 
1674     // notify observers about highlights changes
1675     foreach (int pageNumber, *pagesToNotify)
1676         foreach (DocumentObserver *observer, m_observers)
1677             observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1678 
1679     if (foundAMatch)
1680         emit m_parent->searchFinished(searchID, Document::MatchFound);
1681     else
1682         emit m_parent->searchFinished(searchID, Document::NoMatchFound);
1683 
1684     delete pagesToNotify;
1685 }
1686 
doContinueAllDocumentSearch(void * pagesToNotifySet,void * pageMatchesMap,int currentPage,int searchID)1687 void DocumentPrivate::doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID)
1688 {
1689     QMap<Page *, QVector<RegularAreaRect *>> *pageMatches = static_cast<QMap<Page *, QVector<RegularAreaRect *>> *>(pageMatchesMap);
1690     QSet<int> *pagesToNotify = static_cast<QSet<int> *>(pagesToNotifySet);
1691     RunningSearch *search = m_searches.value(searchID);
1692 
1693     if (m_searchCancelled || !search) {
1694         typedef QVector<RegularAreaRect *> MatchesVector;
1695 
1696         QApplication::restoreOverrideCursor();
1697 
1698         if (search)
1699             search->isCurrentlySearching = false;
1700 
1701         emit m_parent->searchFinished(searchID, Document::SearchCancelled);
1702         foreach (const MatchesVector &mv, *pageMatches)
1703             qDeleteAll(mv);
1704         delete pageMatches;
1705         delete pagesToNotify;
1706         return;
1707     }
1708 
1709     if (currentPage < m_pagesVector.count()) {
1710         // get page (from the first to the last)
1711         Page *page = m_pagesVector.at(currentPage);
1712         int pageNumber = page->number(); // redundant? is it == currentPage ?
1713 
1714         // request search page if needed
1715         if (!page->hasTextPage())
1716             m_parent->requestTextPage(pageNumber);
1717 
1718         // loop on a page adding highlights for all found items
1719         RegularAreaRect *lastMatch = nullptr;
1720         while (true) {
1721             if (lastMatch)
1722                 lastMatch = page->findText(searchID, search->cachedString, NextResult, search->cachedCaseSensitivity, lastMatch);
1723             else
1724                 lastMatch = page->findText(searchID, search->cachedString, FromTop, search->cachedCaseSensitivity);
1725 
1726             if (!lastMatch)
1727                 break;
1728 
1729             // add highlight rect to the matches map
1730             (*pageMatches)[page].append(lastMatch);
1731         }
1732         delete lastMatch;
1733 
1734         QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID] { doContinueAllDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID); });
1735     } else {
1736         // reset cursor to previous shape
1737         QApplication::restoreOverrideCursor();
1738 
1739         search->isCurrentlySearching = false;
1740         bool foundAMatch = pageMatches->count() != 0;
1741         QMap<Page *, QVector<RegularAreaRect *>>::const_iterator it, itEnd;
1742         it = pageMatches->constBegin();
1743         itEnd = pageMatches->constEnd();
1744         for (; it != itEnd; ++it) {
1745             foreach (RegularAreaRect *match, it.value()) {
1746                 it.key()->d->setHighlight(searchID, match, search->cachedColor);
1747                 delete match;
1748             }
1749             search->highlightedPages.insert(it.key()->number());
1750             pagesToNotify->insert(it.key()->number());
1751         }
1752 
1753         foreach (DocumentObserver *observer, m_observers)
1754             observer->notifySetup(m_pagesVector, 0);
1755 
1756         // notify observers about highlights changes
1757         foreach (int pageNumber, *pagesToNotify)
1758             foreach (DocumentObserver *observer, m_observers)
1759                 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1760 
1761         if (foundAMatch)
1762             emit m_parent->searchFinished(searchID, Document::MatchFound);
1763         else
1764             emit m_parent->searchFinished(searchID, Document::NoMatchFound);
1765 
1766         delete pageMatches;
1767         delete pagesToNotify;
1768     }
1769 }
1770 
doContinueGooglesDocumentSearch(void * pagesToNotifySet,void * pageMatchesMap,int currentPage,int searchID,const QStringList & words)1771 void DocumentPrivate::doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList &words)
1772 {
1773     typedef QPair<RegularAreaRect *, QColor> MatchColor;
1774     QMap<Page *, QVector<MatchColor>> *pageMatches = static_cast<QMap<Page *, QVector<MatchColor>> *>(pageMatchesMap);
1775     QSet<int> *pagesToNotify = static_cast<QSet<int> *>(pagesToNotifySet);
1776     RunningSearch *search = m_searches.value(searchID);
1777 
1778     if (m_searchCancelled || !search) {
1779         typedef QVector<MatchColor> MatchesVector;
1780 
1781         QApplication::restoreOverrideCursor();
1782 
1783         if (search)
1784             search->isCurrentlySearching = false;
1785 
1786         emit m_parent->searchFinished(searchID, Document::SearchCancelled);
1787 
1788         foreach (const MatchesVector &mv, *pageMatches) {
1789             foreach (const MatchColor &mc, mv)
1790                 delete mc.first;
1791         }
1792         delete pageMatches;
1793         delete pagesToNotify;
1794         return;
1795     }
1796 
1797     const int wordCount = words.count();
1798     const int hueStep = (wordCount > 1) ? (60 / (wordCount - 1)) : 60;
1799     int baseHue, baseSat, baseVal;
1800     search->cachedColor.getHsv(&baseHue, &baseSat, &baseVal);
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         // loop on a page adding highlights for all found items
1812         bool allMatched = wordCount > 0, anyMatched = false;
1813         for (int w = 0; w < wordCount; w++) {
1814             const QString &word = words[w];
1815             int newHue = baseHue - w * hueStep;
1816             if (newHue < 0)
1817                 newHue += 360;
1818             QColor wordColor = QColor::fromHsv(newHue, baseSat, baseVal);
1819             RegularAreaRect *lastMatch = nullptr;
1820             // add all highlights for current word
1821             bool wordMatched = false;
1822             while (true) {
1823                 if (lastMatch)
1824                     lastMatch = page->findText(searchID, word, NextResult, search->cachedCaseSensitivity, lastMatch);
1825                 else
1826                     lastMatch = page->findText(searchID, word, FromTop, search->cachedCaseSensitivity);
1827 
1828                 if (!lastMatch)
1829                     break;
1830 
1831                 // add highligh rect to the matches map
1832                 (*pageMatches)[page].append(MatchColor(lastMatch, wordColor));
1833                 wordMatched = true;
1834             }
1835             allMatched = allMatched && wordMatched;
1836             anyMatched = anyMatched || wordMatched;
1837         }
1838 
1839         // if not all words are present in page, remove partial highlights
1840         const bool matchAll = search->cachedType == Document::GoogleAll;
1841         if (!allMatched && matchAll) {
1842             QVector<MatchColor> &matches = (*pageMatches)[page];
1843             foreach (const MatchColor &mc, matches)
1844                 delete mc.first;
1845             pageMatches->remove(page);
1846         }
1847 
1848         QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID, words] { doContinueGooglesDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID, words); });
1849     } else {
1850         // reset cursor to previous shape
1851         QApplication::restoreOverrideCursor();
1852 
1853         search->isCurrentlySearching = false;
1854         bool foundAMatch = pageMatches->count() != 0;
1855         QMap<Page *, QVector<MatchColor>>::const_iterator it, itEnd;
1856         it = pageMatches->constBegin();
1857         itEnd = pageMatches->constEnd();
1858         for (; it != itEnd; ++it) {
1859             foreach (const MatchColor &mc, it.value()) {
1860                 it.key()->d->setHighlight(searchID, mc.first, mc.second);
1861                 delete mc.first;
1862             }
1863             search->highlightedPages.insert(it.key()->number());
1864             pagesToNotify->insert(it.key()->number());
1865         }
1866 
1867         // send page lists to update observers (since some filter on bookmarks)
1868         foreach (DocumentObserver *observer, m_observers)
1869             observer->notifySetup(m_pagesVector, 0);
1870 
1871         // notify observers about highlights changes
1872         foreach (int pageNumber, *pagesToNotify)
1873             foreach (DocumentObserver *observer, m_observers)
1874                 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1875 
1876         if (foundAMatch)
1877             emit m_parent->searchFinished(searchID, Document::MatchFound);
1878         else
1879             emit m_parent->searchFinished(searchID, Document::NoMatchFound);
1880 
1881         delete pageMatches;
1882         delete pagesToNotify;
1883     }
1884 }
1885 
documentMetaData(const Generator::DocumentMetaDataKey key,const QVariant & option) const1886 QVariant DocumentPrivate::documentMetaData(const Generator::DocumentMetaDataKey key, const QVariant &option) const
1887 {
1888     switch (key) {
1889     case Generator::PaperColorMetaData: {
1890         bool giveDefault = option.toBool();
1891         QColor color;
1892         if ((SettingsCore::renderMode() == SettingsCore::EnumRenderMode::Paper) && SettingsCore::changeColors()) {
1893             color = SettingsCore::paperColor();
1894         } else if (giveDefault) {
1895             color = Qt::white;
1896         }
1897         return color;
1898     } break;
1899 
1900     case Generator::TextAntialiasMetaData:
1901         switch (SettingsCore::textAntialias()) {
1902         case SettingsCore::EnumTextAntialias::Enabled:
1903             return true;
1904             break;
1905         case SettingsCore::EnumTextAntialias::Disabled:
1906             return false;
1907             break;
1908         }
1909         break;
1910 
1911     case Generator::GraphicsAntialiasMetaData:
1912         switch (SettingsCore::graphicsAntialias()) {
1913         case SettingsCore::EnumGraphicsAntialias::Enabled:
1914             return true;
1915             break;
1916         case SettingsCore::EnumGraphicsAntialias::Disabled:
1917             return false;
1918             break;
1919         }
1920         break;
1921 
1922     case Generator::TextHintingMetaData:
1923         switch (SettingsCore::textHinting()) {
1924         case SettingsCore::EnumTextHinting::Enabled:
1925             return true;
1926             break;
1927         case SettingsCore::EnumTextHinting::Disabled:
1928             return false;
1929             break;
1930         }
1931         break;
1932     }
1933     return QVariant();
1934 }
1935 
isNormalizedRectangleFullyVisible(const Okular::NormalizedRect & rectOfInterest,int rectPage)1936 bool DocumentPrivate::isNormalizedRectangleFullyVisible(const Okular::NormalizedRect &rectOfInterest, int rectPage)
1937 {
1938     bool rectFullyVisible = false;
1939     const QVector<Okular::VisiblePageRect *> &visibleRects = m_parent->visiblePageRects();
1940     QVector<Okular::VisiblePageRect *>::const_iterator vEnd = visibleRects.end();
1941     QVector<Okular::VisiblePageRect *>::const_iterator vIt = visibleRects.begin();
1942 
1943     for (; (vIt != vEnd) && !rectFullyVisible; ++vIt) {
1944         if ((*vIt)->pageNumber == rectPage && (*vIt)->rect.contains(rectOfInterest.left, rectOfInterest.top) && (*vIt)->rect.contains(rectOfInterest.right, rectOfInterest.bottom)) {
1945             rectFullyVisible = true;
1946         }
1947     }
1948     return rectFullyVisible;
1949 }
1950 
1951 struct pdfsyncpoint {
1952     QString file;
1953     qlonglong x;
1954     qlonglong y;
1955     int row;
1956     int column;
1957     int page;
1958 };
1959 
loadSyncFile(const QString & filePath)1960 void DocumentPrivate::loadSyncFile(const QString &filePath)
1961 {
1962     QFile f(filePath + QLatin1String("sync"));
1963     if (!f.open(QIODevice::ReadOnly))
1964         return;
1965 
1966     QTextStream ts(&f);
1967     // first row: core name of the pdf output
1968     const QString coreName = ts.readLine();
1969     // second row: version string, in the form 'Version %u'
1970     const QString versionstr = ts.readLine();
1971     // anchor the pattern with \A and \z to match the entire subject string
1972     // TODO: with Qt 5.12 QRegularExpression::anchoredPattern() can be used instead
1973     QRegularExpression versionre(QStringLiteral("\\AVersion \\d+\\z"), QRegularExpression::CaseInsensitiveOption);
1974     QRegularExpressionMatch match = versionre.match(versionstr);
1975     if (!match.hasMatch()) {
1976         return;
1977     }
1978 
1979     QHash<int, pdfsyncpoint> points;
1980     QStack<QString> fileStack;
1981     int currentpage = -1;
1982     const QLatin1String texStr(".tex");
1983     const QChar spaceChar = QChar::fromLatin1(' ');
1984 
1985     fileStack.push(coreName + texStr);
1986 
1987     const QSizeF dpi = m_generator->dpi();
1988 
1989     QString line;
1990     while (!ts.atEnd()) {
1991         line = ts.readLine();
1992         const QStringList tokens = line.split(spaceChar, QString::SkipEmptyParts);
1993         const int tokenSize = tokens.count();
1994         if (tokenSize < 1)
1995             continue;
1996         if (tokens.first() == QLatin1String("l") && tokenSize >= 3) {
1997             int id = tokens.at(1).toInt();
1998             QHash<int, pdfsyncpoint>::const_iterator it = points.constFind(id);
1999             if (it == points.constEnd()) {
2000                 pdfsyncpoint pt;
2001                 pt.x = 0;
2002                 pt.y = 0;
2003                 pt.row = tokens.at(2).toInt();
2004                 pt.column = 0; // TODO
2005                 pt.page = -1;
2006                 pt.file = fileStack.top();
2007                 points[id] = pt;
2008             }
2009         } else if (tokens.first() == QLatin1String("s") && tokenSize >= 2) {
2010             currentpage = tokens.at(1).toInt() - 1;
2011         } else if (tokens.first() == QLatin1String("p*") && tokenSize >= 4) {
2012             // TODO
2013             qCDebug(OkularCoreDebug) << "PdfSync: 'p*' line ignored";
2014         } else if (tokens.first() == QLatin1String("p") && tokenSize >= 4) {
2015             int id = tokens.at(1).toInt();
2016             QHash<int, pdfsyncpoint>::iterator it = points.find(id);
2017             if (it != points.end()) {
2018                 it->x = tokens.at(2).toInt();
2019                 it->y = tokens.at(3).toInt();
2020                 it->page = currentpage;
2021             }
2022         } else if (line.startsWith(QLatin1Char('(')) && tokenSize == 1) {
2023             QString newfile = line;
2024             // chop the leading '('
2025             newfile.remove(0, 1);
2026             if (!newfile.endsWith(texStr)) {
2027                 newfile += texStr;
2028             }
2029             fileStack.push(newfile);
2030         } else if (line == QLatin1String(")")) {
2031             if (!fileStack.isEmpty()) {
2032                 fileStack.pop();
2033             } else
2034                 qCDebug(OkularCoreDebug) << "PdfSync: going one level down too much";
2035         } else
2036             qCDebug(OkularCoreDebug).nospace() << "PdfSync: unknown line format: '" << line << "'";
2037     }
2038 
2039     QVector<QLinkedList<Okular::SourceRefObjectRect *>> refRects(m_pagesVector.size());
2040     for (const pdfsyncpoint &pt : qAsConst(points)) {
2041         // drop pdfsync points not completely valid
2042         if (pt.page < 0 || pt.page >= m_pagesVector.size())
2043             continue;
2044 
2045         // magic numbers for TeX's RSU's (Ridiculously Small Units) conversion to pixels
2046         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()));
2047         QString file = pt.file;
2048         Okular::SourceReference *sourceRef = new Okular::SourceReference(file, pt.row, pt.column);
2049         refRects[pt.page].append(new Okular::SourceRefObjectRect(p, sourceRef));
2050     }
2051     for (int i = 0; i < refRects.size(); ++i)
2052         if (!refRects.at(i).isEmpty())
2053             m_pagesVector[i]->setSourceReferences(refRects.at(i));
2054 }
2055 
clearAndWaitForRequests()2056 void DocumentPrivate::clearAndWaitForRequests()
2057 {
2058     m_pixmapRequestsMutex.lock();
2059     QLinkedList<PixmapRequest *>::const_iterator sIt = m_pixmapRequestsStack.constBegin();
2060     QLinkedList<PixmapRequest *>::const_iterator sEnd = m_pixmapRequestsStack.constEnd();
2061     for (; sIt != sEnd; ++sIt)
2062         delete *sIt;
2063     m_pixmapRequestsStack.clear();
2064     m_pixmapRequestsMutex.unlock();
2065 
2066     QEventLoop loop;
2067     bool startEventLoop = false;
2068     do {
2069         m_pixmapRequestsMutex.lock();
2070         startEventLoop = !m_executingPixmapRequests.isEmpty();
2071 
2072         if (m_generator->hasFeature(Generator::SupportsCancelling)) {
2073             for (PixmapRequest *executingRequest : qAsConst(m_executingPixmapRequests))
2074                 executingRequest->d->mShouldAbortRender = 1;
2075 
2076             if (m_generator->d_ptr->mTextPageGenerationThread)
2077                 m_generator->d_ptr->mTextPageGenerationThread->abortExtraction();
2078         }
2079 
2080         m_pixmapRequestsMutex.unlock();
2081         if (startEventLoop) {
2082             m_closingLoop = &loop;
2083             loop.exec();
2084             m_closingLoop = nullptr;
2085         }
2086     } while (startEventLoop);
2087 }
2088 
findFieldPageNumber(Okular::FormField * field)2089 int DocumentPrivate::findFieldPageNumber(Okular::FormField *field)
2090 {
2091     // Lookup the page of the FormField
2092     int foundPage = -1;
2093     for (uint pageIdx = 0, nPages = m_parent->pages(); pageIdx < nPages; pageIdx++) {
2094         const Page *p = m_parent->page(pageIdx);
2095         if (p && p->formFields().contains(field)) {
2096             foundPage = static_cast<int>(pageIdx);
2097             break;
2098         }
2099     }
2100     return foundPage;
2101 }
2102 
executeScriptEvent(const std::shared_ptr<Event> & event,const Okular::ScriptAction * linkscript)2103 void DocumentPrivate::executeScriptEvent(const std::shared_ptr<Event> &event, const Okular::ScriptAction *linkscript)
2104 {
2105     if (!m_scripter) {
2106         m_scripter = new Scripter(this);
2107     }
2108     m_scripter->setEvent(event.get());
2109     m_scripter->execute(linkscript->scriptType(), linkscript->script());
2110 
2111     // Clear out the event after execution
2112     m_scripter->setEvent(nullptr);
2113 }
2114 
Document(QWidget * widget)2115 Document::Document(QWidget *widget)
2116     : QObject(nullptr)
2117     , d(new DocumentPrivate(this))
2118 {
2119     d->m_widget = widget;
2120     d->m_bookmarkManager = new BookmarkManager(d);
2121     d->m_viewportIterator = d->m_viewportHistory.insert(d->m_viewportHistory.end(), DocumentViewport());
2122     d->m_undoStack = new QUndoStack(this);
2123 
2124     connect(SettingsCore::self(), &SettingsCore::configChanged, this, [this] { d->_o_configChanged(); });
2125     connect(d->m_undoStack, &QUndoStack::canUndoChanged, this, &Document::canUndoChanged);
2126     connect(d->m_undoStack, &QUndoStack::canRedoChanged, this, &Document::canRedoChanged);
2127     connect(d->m_undoStack, &QUndoStack::cleanChanged, this, &Document::undoHistoryCleanChanged);
2128 
2129     qRegisterMetaType<Okular::FontInfo>();
2130 }
2131 
~Document()2132 Document::~Document()
2133 {
2134     // delete generator, pages, and related stuff
2135     closeDocument();
2136 
2137     QSet<View *>::const_iterator viewIt = d->m_views.constBegin(), viewEnd = d->m_views.constEnd();
2138     for (; viewIt != viewEnd; ++viewIt) {
2139         View *v = *viewIt;
2140         v->d_func()->document = nullptr;
2141     }
2142 
2143     // delete the bookmark manager
2144     delete d->m_bookmarkManager;
2145 
2146     // delete the loaded generators
2147     QHash<QString, GeneratorInfo>::const_iterator it = d->m_loadedGenerators.constBegin(), itEnd = d->m_loadedGenerators.constEnd();
2148     for (; it != itEnd; ++it)
2149         d->unloadGenerator(it.value());
2150     d->m_loadedGenerators.clear();
2151 
2152     // delete the private structure
2153     delete d;
2154 }
2155 
docDataFileName(const QUrl & url,qint64 document_size)2156 QString DocumentPrivate::docDataFileName(const QUrl &url, qint64 document_size)
2157 {
2158     QString fn = url.fileName();
2159     fn = QString::number(document_size) + QLatin1Char('.') + fn + QStringLiteral(".xml");
2160     QString docdataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/okular/docdata");
2161     // make sure that the okular/docdata/ directory exists (probably this used to be handled by KStandardDirs)
2162     if (!QFileInfo::exists(docdataDir)) {
2163         qCDebug(OkularCoreDebug) << "creating docdata folder" << docdataDir;
2164         QDir().mkpath(docdataDir);
2165     }
2166     QString newokularfile = docdataDir + QLatin1Char('/') + fn;
2167     // we don't want to accidentally migrate old files when running unit tests
2168     if (!QFile::exists(newokularfile) && !QStandardPaths::isTestModeEnabled()) {
2169         // see if an KDE4 file still exists
2170         static Kdelibs4Migration k4migration;
2171         QString oldfile = k4migration.locateLocal("data", QStringLiteral("okular/docdata/") + fn);
2172         if (oldfile.isEmpty()) {
2173             oldfile = k4migration.locateLocal("data", QStringLiteral("kpdf/") + fn);
2174         }
2175         if (!oldfile.isEmpty() && QFile::exists(oldfile)) {
2176             // ### copy or move?
2177             if (!QFile::copy(oldfile, newokularfile))
2178                 return QString();
2179         }
2180     }
2181     return newokularfile;
2182 }
2183 
availableGenerators()2184 QVector<KPluginMetaData> DocumentPrivate::availableGenerators()
2185 {
2186     static QVector<KPluginMetaData> result;
2187     if (result.isEmpty()) {
2188         result = KPluginLoader::findPlugins(QStringLiteral("okular/generators"));
2189     }
2190     return result;
2191 }
2192 
generatorForMimeType(const QMimeType & type,QWidget * widget,const QVector<KPluginMetaData> & triedOffers)2193 KPluginMetaData DocumentPrivate::generatorForMimeType(const QMimeType &type, QWidget *widget, const QVector<KPluginMetaData> &triedOffers)
2194 {
2195     // First try to find an exact match, and then look for more general ones (e. g. the plain text one)
2196     // Ideally we would rank these by "closeness", but that might be overdoing it
2197 
2198     const QVector<KPluginMetaData> available = availableGenerators();
2199     QVector<KPluginMetaData> offers;
2200     QVector<KPluginMetaData> exactMatches;
2201 
2202     QMimeDatabase mimeDatabase;
2203 
2204     for (const KPluginMetaData &md : available) {
2205         if (triedOffers.contains(md))
2206             continue;
2207 
2208         const QStringList mimetypes = md.mimeTypes();
2209         for (const QString &supported : mimetypes) {
2210             QMimeType mimeType = mimeDatabase.mimeTypeForName(supported);
2211             if (mimeType == type && !exactMatches.contains(md)) {
2212                 exactMatches << md;
2213             }
2214 
2215             if (type.inherits(supported) && !offers.contains(md)) {
2216                 offers << md;
2217             }
2218         }
2219     }
2220 
2221     if (!exactMatches.isEmpty()) {
2222         offers = exactMatches;
2223     }
2224 
2225     if (offers.isEmpty()) {
2226         return KPluginMetaData();
2227     }
2228     int hRank = 0;
2229     // best ranked offer search
2230     int offercount = offers.size();
2231     if (offercount > 1) {
2232         // sort the offers: the offers with an higher priority come before
2233         auto cmp = [](const KPluginMetaData &s1, const KPluginMetaData &s2) {
2234             const QString property = QStringLiteral("X-KDE-Priority");
2235             return s1.rawData()[property].toInt() > s2.rawData()[property].toInt();
2236         };
2237         std::stable_sort(offers.begin(), offers.end(), cmp);
2238 
2239         if (SettingsCore::chooseGenerators()) {
2240             QStringList list;
2241             for (int i = 0; i < offercount; ++i) {
2242                 list << offers.at(i).pluginId();
2243             }
2244             ChooseEngineDialog choose(list, type, widget);
2245 
2246             if (choose.exec() == QDialog::Rejected)
2247                 return KPluginMetaData();
2248 
2249             hRank = choose.selectedGenerator();
2250         }
2251     }
2252     Q_ASSERT(hRank < offers.size());
2253     return offers.at(hRank);
2254 }
2255 
openDocument(const QString & docFile,const QUrl & url,const QMimeType & _mime,const QString & password)2256 Document::OpenResult Document::openDocument(const QString &docFile, const QUrl &url, const QMimeType &_mime, const QString &password)
2257 {
2258     QMimeDatabase db;
2259     QMimeType mime = _mime;
2260     QByteArray filedata;
2261     int fd = -1;
2262     if (url.scheme() == QLatin1String("fd")) {
2263         bool ok;
2264         fd = url.path().midRef(1).toInt(&ok);
2265         if (!ok) {
2266             return OpenError;
2267         }
2268     } else if (url.fileName() == QLatin1String("-")) {
2269         fd = 0;
2270     }
2271     bool triedMimeFromFileContent = false;
2272     if (fd < 0) {
2273         if (!mime.isValid())
2274             return OpenError;
2275 
2276         d->m_url = url;
2277         d->m_docFileName = docFile;
2278 
2279         if (!d->updateMetadataXmlNameAndDocSize())
2280             return OpenError;
2281     } else {
2282         QFile qstdin;
2283         const bool ret = qstdin.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle);
2284         if (!ret) {
2285             qWarning() << "failed to read" << url << filedata;
2286             return OpenError;
2287         }
2288 
2289         filedata = qstdin.readAll();
2290         mime = db.mimeTypeForData(filedata);
2291         if (!mime.isValid() || mime.isDefault())
2292             return OpenError;
2293         d->m_docSize = filedata.size();
2294         triedMimeFromFileContent = true;
2295     }
2296 
2297     const bool fromFileDescriptor = fd >= 0;
2298 
2299     // 0. load Generator
2300     // request only valid non-disabled plugins suitable for the mimetype
2301     KPluginMetaData offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2302     if (!offer.isValid() && !triedMimeFromFileContent) {
2303         QMimeType newmime = db.mimeTypeForFile(docFile, QMimeDatabase::MatchContent);
2304         triedMimeFromFileContent = true;
2305         if (newmime != mime) {
2306             mime = newmime;
2307             offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2308         }
2309         if (!offer.isValid()) {
2310             // There's still no offers, do a final mime search based on the filename
2311             // We need this because sometimes (e.g. when downloading from a webserver) the mimetype we
2312             // use is the one fed by the server, that may be wrong
2313             newmime = db.mimeTypeForUrl(url);
2314 
2315             if (!newmime.isDefault() && newmime != mime) {
2316                 mime = newmime;
2317                 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2318             }
2319         }
2320     }
2321     if (!offer.isValid()) {
2322         d->m_openError = i18n("Can not find a plugin which is able to handle the document being passed.");
2323         emit error(d->m_openError, -1);
2324         qCWarning(OkularCoreDebug).nospace() << "No plugin for mimetype '" << mime.name() << "'.";
2325         return OpenError;
2326     }
2327 
2328     // 1. load Document
2329     OpenResult openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2330     if (openResult == OpenError) {
2331         QVector<KPluginMetaData> triedOffers;
2332         triedOffers << offer;
2333         offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2334 
2335         while (offer.isValid()) {
2336             openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2337 
2338             if (openResult == OpenError) {
2339                 triedOffers << offer;
2340                 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2341             } else
2342                 break;
2343         }
2344 
2345         if (openResult == OpenError && !triedMimeFromFileContent) {
2346             QMimeType newmime = db.mimeTypeForFile(docFile, QMimeDatabase::MatchContent);
2347             triedMimeFromFileContent = true;
2348             if (newmime != mime) {
2349                 mime = newmime;
2350                 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2351                 while (offer.isValid()) {
2352                     openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2353 
2354                     if (openResult == OpenError) {
2355                         triedOffers << offer;
2356                         offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2357                     } else
2358                         break;
2359                 }
2360             }
2361         }
2362 
2363         if (openResult == OpenSuccess) {
2364             // Clear errors, since we're trying various generators, maybe one of them errored out
2365             // but we finally succeeded
2366             // TODO one can still see the error message animating out but since this is a very rare
2367             //      condition we can leave this for future work
2368             emit error(QString(), -1);
2369         }
2370     }
2371     if (openResult != OpenSuccess) {
2372         return openResult;
2373     }
2374 
2375     // no need to check for the existence of a synctex file, no parser will be
2376     // created if none exists
2377     d->m_synctex_scanner = synctex_scanner_new_with_output_file(QFile::encodeName(docFile).constData(), nullptr, 1);
2378     if (!d->m_synctex_scanner && QFile::exists(docFile + QLatin1String("sync"))) {
2379         d->loadSyncFile(docFile);
2380     }
2381 
2382     d->m_generatorName = offer.pluginId();
2383     d->m_pageController = new PageController();
2384     connect(d->m_pageController, &PageController::rotationFinished, this, [this](int p, Okular::Page *op) { d->rotationFinished(p, op); });
2385 
2386     for (Page *p : qAsConst(d->m_pagesVector))
2387         p->d->m_doc = d;
2388 
2389     d->m_metadataLoadingCompleted = false;
2390     d->m_docdataMigrationNeeded = false;
2391 
2392     // 2. load Additional Data (bookmarks, local annotations and metadata) about the document
2393     if (d->m_archiveData) {
2394         // QTemporaryFile is weird and will return false in exists if fileName wasn't called before
2395         d->m_archiveData->metadataFile.fileName();
2396         d->loadDocumentInfo(d->m_archiveData->metadataFile, LoadPageInfo);
2397         d->loadDocumentInfo(LoadGeneralInfo);
2398     } else {
2399         if (d->loadDocumentInfo(LoadPageInfo))
2400             d->m_docdataMigrationNeeded = true;
2401         d->loadDocumentInfo(LoadGeneralInfo);
2402     }
2403 
2404     d->m_metadataLoadingCompleted = true;
2405     d->m_bookmarkManager->setUrl(d->m_url);
2406 
2407     // 3. setup observers internal lists and data
2408     foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged));
2409 
2410     // 4. set initial page (restoring the page saved in xml if loaded)
2411     DocumentViewport loadedViewport = (*d->m_viewportIterator);
2412     if (loadedViewport.isValid()) {
2413         (*d->m_viewportIterator) = DocumentViewport();
2414         if (loadedViewport.pageNumber >= (int)d->m_pagesVector.size())
2415             loadedViewport.pageNumber = d->m_pagesVector.size() - 1;
2416     } else
2417         loadedViewport.pageNumber = 0;
2418     setViewport(loadedViewport);
2419 
2420     // start bookmark saver timer
2421     if (!d->m_saveBookmarksTimer) {
2422         d->m_saveBookmarksTimer = new QTimer(this);
2423         connect(d->m_saveBookmarksTimer, &QTimer::timeout, this, [this] { d->saveDocumentInfo(); });
2424     }
2425     d->m_saveBookmarksTimer->start(5 * 60 * 1000);
2426 
2427     // start memory check timer
2428     if (!d->m_memCheckTimer) {
2429         d->m_memCheckTimer = new QTimer(this);
2430         connect(d->m_memCheckTimer, &QTimer::timeout, this, [this] { d->slotTimedMemoryCheck(); });
2431     }
2432     d->m_memCheckTimer->start(kMemCheckTime);
2433 
2434     const DocumentViewport nextViewport = d->nextDocumentViewport();
2435     if (nextViewport.isValid()) {
2436         setViewport(nextViewport);
2437         d->m_nextDocumentViewport = DocumentViewport();
2438         d->m_nextDocumentDestination = QString();
2439     }
2440 
2441     AudioPlayer::instance()->d->m_currentDocument = fromFileDescriptor ? QUrl() : d->m_url;
2442 
2443     const QStringList docScripts = d->m_generator->metaData(QStringLiteral("DocumentScripts"), QStringLiteral("JavaScript")).toStringList();
2444     if (!docScripts.isEmpty()) {
2445         d->m_scripter = new Scripter(d);
2446         for (const QString &docscript : docScripts) {
2447             d->m_scripter->execute(JavaScript, docscript);
2448         }
2449     }
2450 
2451     return OpenSuccess;
2452 }
2453 
updateMetadataXmlNameAndDocSize()2454 bool DocumentPrivate::updateMetadataXmlNameAndDocSize()
2455 {
2456     // m_docFileName is always local so we can use QFileInfo on it
2457     QFileInfo fileReadTest(m_docFileName);
2458     if (!fileReadTest.isFile() && !fileReadTest.isReadable())
2459         return false;
2460 
2461     m_docSize = fileReadTest.size();
2462 
2463     // determine the related "xml document-info" filename
2464     if (m_url.isLocalFile()) {
2465         const QString filePath = docDataFileName(m_url, m_docSize);
2466         qCDebug(OkularCoreDebug) << "Metadata file is now:" << filePath;
2467         m_xmlFileName = filePath;
2468     } else {
2469         qCDebug(OkularCoreDebug) << "Metadata file: disabled";
2470         m_xmlFileName = QString();
2471     }
2472 
2473     return true;
2474 }
2475 
guiClient()2476 KXMLGUIClient *Document::guiClient()
2477 {
2478     if (d->m_generator) {
2479         Okular::GuiInterface *iface = qobject_cast<Okular::GuiInterface *>(d->m_generator);
2480         if (iface)
2481             return iface->guiClient();
2482     }
2483     return nullptr;
2484 }
2485 
closeDocument()2486 void Document::closeDocument()
2487 {
2488     // check if there's anything to close...
2489     if (!d->m_generator)
2490         return;
2491 
2492     emit aboutToClose();
2493 
2494     delete d->m_pageController;
2495     d->m_pageController = nullptr;
2496 
2497     delete d->m_scripter;
2498     d->m_scripter = nullptr;
2499 
2500     // remove requests left in queue
2501     d->clearAndWaitForRequests();
2502 
2503     if (d->m_fontThread) {
2504         disconnect(d->m_fontThread, nullptr, this, nullptr);
2505         d->m_fontThread->stopExtraction();
2506         d->m_fontThread->wait();
2507         d->m_fontThread = nullptr;
2508     }
2509 
2510     // stop any audio playback
2511     AudioPlayer::instance()->stopPlaybacks();
2512 
2513     // close the current document and save document info if a document is still opened
2514     if (d->m_generator && d->m_pagesVector.size() > 0) {
2515         d->saveDocumentInfo();
2516         d->m_generator->closeDocument();
2517     }
2518 
2519     if (d->m_synctex_scanner) {
2520         synctex_scanner_free(d->m_synctex_scanner);
2521         d->m_synctex_scanner = nullptr;
2522     }
2523 
2524     // stop timers
2525     if (d->m_memCheckTimer)
2526         d->m_memCheckTimer->stop();
2527     if (d->m_saveBookmarksTimer)
2528         d->m_saveBookmarksTimer->stop();
2529 
2530     if (d->m_generator) {
2531         // disconnect the generator from this document ...
2532         d->m_generator->d_func()->m_document = nullptr;
2533         // .. and this document from the generator signals
2534         disconnect(d->m_generator, nullptr, this, nullptr);
2535 
2536         QHash<QString, GeneratorInfo>::const_iterator genIt = d->m_loadedGenerators.constFind(d->m_generatorName);
2537         Q_ASSERT(genIt != d->m_loadedGenerators.constEnd());
2538     }
2539     d->m_generator = nullptr;
2540     d->m_generatorName = QString();
2541     d->m_url = QUrl();
2542     d->m_walletGenerator = nullptr;
2543     d->m_docFileName = QString();
2544     d->m_xmlFileName = QString();
2545     delete d->m_tempFile;
2546     d->m_tempFile = nullptr;
2547     delete d->m_archiveData;
2548     d->m_archiveData = nullptr;
2549     d->m_docSize = -1;
2550     d->m_exportCached = false;
2551     d->m_exportFormats.clear();
2552     d->m_exportToText = ExportFormat();
2553     d->m_fontsCached = false;
2554     d->m_fontsCache.clear();
2555     d->m_rotation = Rotation0;
2556 
2557     // send an empty list to observers (to free their data)
2558     foreachObserver(notifySetup(QVector<Page *>(), DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged));
2559 
2560     // delete pages and clear 'd->m_pagesVector' container
2561     QVector<Page *>::const_iterator pIt = d->m_pagesVector.constBegin();
2562     QVector<Page *>::const_iterator pEnd = d->m_pagesVector.constEnd();
2563     for (; pIt != pEnd; ++pIt)
2564         delete *pIt;
2565     d->m_pagesVector.clear();
2566 
2567     // clear 'memory allocation' descriptors
2568     qDeleteAll(d->m_allocatedPixmaps);
2569     d->m_allocatedPixmaps.clear();
2570 
2571     // clear 'running searches' descriptors
2572     QMap<int, RunningSearch *>::const_iterator rIt = d->m_searches.constBegin();
2573     QMap<int, RunningSearch *>::const_iterator rEnd = d->m_searches.constEnd();
2574     for (; rIt != rEnd; ++rIt)
2575         delete *rIt;
2576     d->m_searches.clear();
2577 
2578     // clear the visible areas and notify the observers
2579     QVector<VisiblePageRect *>::const_iterator vIt = d->m_pageRects.constBegin();
2580     QVector<VisiblePageRect *>::const_iterator vEnd = d->m_pageRects.constEnd();
2581     for (; vIt != vEnd; ++vIt)
2582         delete *vIt;
2583     d->m_pageRects.clear();
2584     foreachObserver(notifyVisibleRectsChanged());
2585 
2586     // reset internal variables
2587 
2588     d->m_viewportHistory.clear();
2589     d->m_viewportHistory.append(DocumentViewport());
2590     d->m_viewportIterator = d->m_viewportHistory.begin();
2591     d->m_allocatedPixmapsTotalMemory = 0;
2592     d->m_allocatedTextPagesFifo.clear();
2593     d->m_pageSize = PageSize();
2594     d->m_pageSizes.clear();
2595 
2596     d->m_documentInfo = DocumentInfo();
2597     d->m_documentInfoAskedKeys.clear();
2598 
2599     AudioPlayer::instance()->d->m_currentDocument = QUrl();
2600 
2601     d->m_undoStack->clear();
2602     d->m_docdataMigrationNeeded = false;
2603 
2604 #if HAVE_MALLOC_TRIM
2605     // trim unused memory, glibc should do this but it seems it does not
2606     // this can greatly decrease the [perceived] memory consumption of okular
2607     // see: https://sourceware.org/bugzilla/show_bug.cgi?id=14827
2608     malloc_trim(0);
2609 #endif
2610 }
2611 
addObserver(DocumentObserver * pObserver)2612 void Document::addObserver(DocumentObserver *pObserver)
2613 {
2614     Q_ASSERT(!d->m_observers.contains(pObserver));
2615     d->m_observers << pObserver;
2616 
2617     // if the observer is added while a document is already opened, tell it
2618     if (!d->m_pagesVector.isEmpty()) {
2619         pObserver->notifySetup(d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged);
2620         pObserver->notifyViewportChanged(false /*disables smoothMove*/);
2621     }
2622 }
2623 
removeObserver(DocumentObserver * pObserver)2624 void Document::removeObserver(DocumentObserver *pObserver)
2625 {
2626     // remove observer from the set. it won't receive notifications anymore
2627     if (d->m_observers.contains(pObserver)) {
2628         // free observer's pixmap data
2629         QVector<Page *>::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd();
2630         for (; it != end; ++it)
2631             (*it)->deletePixmap(pObserver);
2632 
2633         // [MEM] free observer's allocation descriptors
2634         QLinkedList<AllocatedPixmap *>::iterator aIt = d->m_allocatedPixmaps.begin();
2635         QLinkedList<AllocatedPixmap *>::iterator aEnd = d->m_allocatedPixmaps.end();
2636         while (aIt != aEnd) {
2637             AllocatedPixmap *p = *aIt;
2638             if (p->observer == pObserver) {
2639                 aIt = d->m_allocatedPixmaps.erase(aIt);
2640                 delete p;
2641             } else
2642                 ++aIt;
2643         }
2644 
2645         for (PixmapRequest *executingRequest : qAsConst(d->m_executingPixmapRequests)) {
2646             if (executingRequest->observer() == pObserver) {
2647                 d->cancelRenderingBecauseOf(executingRequest, nullptr);
2648             }
2649         }
2650 
2651         // remove observer entry from the set
2652         d->m_observers.remove(pObserver);
2653     }
2654 }
2655 
reparseConfig()2656 void Document::reparseConfig()
2657 {
2658     // reparse generator config and if something changed clear Pages
2659     bool configchanged = false;
2660     if (d->m_generator) {
2661         Okular::ConfigInterface *iface = qobject_cast<Okular::ConfigInterface *>(d->m_generator);
2662         if (iface)
2663             configchanged = iface->reparseConfig();
2664     }
2665     if (configchanged) {
2666         // invalidate pixmaps
2667         QVector<Page *>::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd();
2668         for (; it != end; ++it) {
2669             (*it)->deletePixmaps();
2670         }
2671 
2672         // [MEM] remove allocation descriptors
2673         qDeleteAll(d->m_allocatedPixmaps);
2674         d->m_allocatedPixmaps.clear();
2675         d->m_allocatedPixmapsTotalMemory = 0;
2676 
2677         // send reload signals to observers
2678         foreachObserver(notifyContentsCleared(DocumentObserver::Pixmap));
2679     }
2680 
2681     // free memory if in 'low' profile
2682     if (SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !d->m_allocatedPixmaps.isEmpty() && !d->m_pagesVector.isEmpty())
2683         d->cleanupPixmapMemory();
2684 }
2685 
isOpened() const2686 bool Document::isOpened() const
2687 {
2688     return d->m_generator;
2689 }
2690 
canConfigurePrinter() const2691 bool Document::canConfigurePrinter() const
2692 {
2693     if (d->m_generator) {
2694         Okular::PrintInterface *iface = qobject_cast<Okular::PrintInterface *>(d->m_generator);
2695         return iface ? true : false;
2696     } else
2697         return false;
2698 }
2699 
sign(const NewSignatureData & data,const QString & newPath)2700 bool Document::sign(const NewSignatureData &data, const QString &newPath)
2701 {
2702     if (d->m_generator->canSign()) {
2703         return d->m_generator->sign(data, newPath);
2704     } else {
2705         return false;
2706     }
2707 }
2708 
certificateStore() const2709 Okular::CertificateStore *Document::certificateStore() const
2710 {
2711     return d->m_generator ? d->m_generator->certificateStore() : nullptr;
2712 }
2713 
documentInfo() const2714 DocumentInfo Document::documentInfo() const
2715 {
2716     QSet<DocumentInfo::Key> keys;
2717     for (Okular::DocumentInfo::Key ks = Okular::DocumentInfo::Title; ks < Okular::DocumentInfo::Invalid; ks = Okular::DocumentInfo::Key(ks + 1)) {
2718         keys << ks;
2719     }
2720 
2721     return documentInfo(keys);
2722 }
2723 
documentInfo(const QSet<DocumentInfo::Key> & keys) const2724 DocumentInfo Document::documentInfo(const QSet<DocumentInfo::Key> &keys) const
2725 {
2726     DocumentInfo result = d->m_documentInfo;
2727     const QSet<DocumentInfo::Key> missingKeys = keys - d->m_documentInfoAskedKeys;
2728 
2729     if (d->m_generator && !missingKeys.isEmpty()) {
2730         DocumentInfo info = d->m_generator->generateDocumentInfo(missingKeys);
2731 
2732         if (missingKeys.contains(DocumentInfo::FilePath)) {
2733             info.set(DocumentInfo::FilePath, currentDocument().toDisplayString());
2734         }
2735 
2736         if (d->m_docSize != -1 && missingKeys.contains(DocumentInfo::DocumentSize)) {
2737             const QString sizeString = KFormat().formatByteSize(d->m_docSize);
2738             info.set(DocumentInfo::DocumentSize, sizeString);
2739         }
2740         if (missingKeys.contains(DocumentInfo::PagesSize)) {
2741             const QString pagesSize = d->pagesSizeString();
2742             if (!pagesSize.isEmpty()) {
2743                 info.set(DocumentInfo::PagesSize, pagesSize);
2744             }
2745         }
2746 
2747         if (missingKeys.contains(DocumentInfo::Pages) && info.get(DocumentInfo::Pages).isEmpty()) {
2748             info.set(DocumentInfo::Pages, QString::number(this->pages()));
2749         }
2750 
2751         d->m_documentInfo.d->values.unite(info.d->values);
2752         d->m_documentInfo.d->titles.unite(info.d->titles);
2753         result.d->values.unite(info.d->values);
2754         result.d->titles.unite(info.d->titles);
2755     }
2756     d->m_documentInfoAskedKeys += keys;
2757 
2758     return result;
2759 }
2760 
documentSynopsis() const2761 const DocumentSynopsis *Document::documentSynopsis() const
2762 {
2763     return d->m_generator ? d->m_generator->generateDocumentSynopsis() : nullptr;
2764 }
2765 
startFontReading()2766 void Document::startFontReading()
2767 {
2768     if (!d->m_generator || !d->m_generator->hasFeature(Generator::FontInfo) || d->m_fontThread)
2769         return;
2770 
2771     if (d->m_fontsCached) {
2772         // in case we have cached fonts, simulate a reading
2773         // this way the API is the same, and users no need to care about the
2774         // internal caching
2775         for (int i = 0; i < d->m_fontsCache.count(); ++i) {
2776             emit gotFont(d->m_fontsCache.at(i));
2777             emit fontReadingProgress(i / pages());
2778         }
2779         emit fontReadingEnded();
2780         return;
2781     }
2782 
2783     d->m_fontThread = new FontExtractionThread(d->m_generator, pages());
2784     connect(d->m_fontThread, &FontExtractionThread::gotFont, this, [this](const Okular::FontInfo &f) { d->fontReadingGotFont(f); });
2785     connect(d->m_fontThread.data(), &FontExtractionThread::progress, this, [this](int p) { d->slotFontReadingProgress(p); });
2786 
2787     d->m_fontThread->startExtraction(/*d->m_generator->hasFeature( Generator::Threaded )*/ true);
2788 }
2789 
stopFontReading()2790 void Document::stopFontReading()
2791 {
2792     if (!d->m_fontThread)
2793         return;
2794 
2795     disconnect(d->m_fontThread, nullptr, this, nullptr);
2796     d->m_fontThread->stopExtraction();
2797     d->m_fontThread = nullptr;
2798     d->m_fontsCache.clear();
2799 }
2800 
canProvideFontInformation() const2801 bool Document::canProvideFontInformation() const
2802 {
2803     return d->m_generator ? d->m_generator->hasFeature(Generator::FontInfo) : false;
2804 }
2805 
canSign() const2806 bool Document::canSign() const
2807 {
2808     return d->m_generator ? d->m_generator->canSign() : false;
2809 }
2810 
embeddedFiles() const2811 const QList<EmbeddedFile *> *Document::embeddedFiles() const
2812 {
2813     return d->m_generator ? d->m_generator->embeddedFiles() : nullptr;
2814 }
2815 
page(int n) const2816 const Page *Document::page(int n) const
2817 {
2818     return (n >= 0 && n < d->m_pagesVector.count()) ? d->m_pagesVector.at(n) : nullptr;
2819 }
2820 
viewport() const2821 const DocumentViewport &Document::viewport() const
2822 {
2823     return (*d->m_viewportIterator);
2824 }
2825 
visiblePageRects() const2826 const QVector<VisiblePageRect *> &Document::visiblePageRects() const
2827 {
2828     return d->m_pageRects;
2829 }
2830 
setVisiblePageRects(const QVector<VisiblePageRect * > & visiblePageRects,DocumentObserver * excludeObserver)2831 void Document::setVisiblePageRects(const QVector<VisiblePageRect *> &visiblePageRects, DocumentObserver *excludeObserver)
2832 {
2833     QVector<VisiblePageRect *>::const_iterator vIt = d->m_pageRects.constBegin();
2834     QVector<VisiblePageRect *>::const_iterator vEnd = d->m_pageRects.constEnd();
2835     for (; vIt != vEnd; ++vIt)
2836         delete *vIt;
2837     d->m_pageRects = visiblePageRects;
2838     // notify change to all other (different from id) observers
2839     foreach (DocumentObserver *o, d->m_observers)
2840         if (o != excludeObserver)
2841             o->notifyVisibleRectsChanged();
2842 }
2843 
currentPage() const2844 uint Document::currentPage() const
2845 {
2846     return (*d->m_viewportIterator).pageNumber;
2847 }
2848 
pages() const2849 uint Document::pages() const
2850 {
2851     return d->m_pagesVector.size();
2852 }
2853 
currentDocument() const2854 QUrl Document::currentDocument() const
2855 {
2856     return d->m_url;
2857 }
2858 
isAllowed(Permission action) const2859 bool Document::isAllowed(Permission action) const
2860 {
2861     if (action == Okular::AllowNotes && (d->m_docdataMigrationNeeded || !d->m_annotationEditingEnabled))
2862         return false;
2863     if (action == Okular::AllowFillForms && d->m_docdataMigrationNeeded)
2864         return false;
2865 
2866 #if !OKULAR_FORCE_DRM
2867     if (KAuthorized::authorize(QStringLiteral("skip_drm")) && !SettingsCore::obeyDRM())
2868         return true;
2869 #endif
2870 
2871     return d->m_generator ? d->m_generator->isAllowed(action) : false;
2872 }
2873 
supportsSearching() const2874 bool Document::supportsSearching() const
2875 {
2876     return d->m_generator ? d->m_generator->hasFeature(Generator::TextExtraction) : false;
2877 }
2878 
supportsPageSizes() const2879 bool Document::supportsPageSizes() const
2880 {
2881     return d->m_generator ? d->m_generator->hasFeature(Generator::PageSizes) : false;
2882 }
2883 
supportsTiles() const2884 bool Document::supportsTiles() const
2885 {
2886     return d->m_generator ? d->m_generator->hasFeature(Generator::TiledRendering) : false;
2887 }
2888 
pageSizes() const2889 PageSize::List Document::pageSizes() const
2890 {
2891     if (d->m_generator) {
2892         if (d->m_pageSizes.isEmpty())
2893             d->m_pageSizes = d->m_generator->pageSizes();
2894         return d->m_pageSizes;
2895     }
2896     return PageSize::List();
2897 }
2898 
canExportToText() const2899 bool Document::canExportToText() const
2900 {
2901     if (!d->m_generator)
2902         return false;
2903 
2904     d->cacheExportFormats();
2905     return !d->m_exportToText.isNull();
2906 }
2907 
exportToText(const QString & fileName) const2908 bool Document::exportToText(const QString &fileName) const
2909 {
2910     if (!d->m_generator)
2911         return false;
2912 
2913     d->cacheExportFormats();
2914     if (d->m_exportToText.isNull())
2915         return false;
2916 
2917     return d->m_generator->exportTo(fileName, d->m_exportToText);
2918 }
2919 
exportFormats() const2920 ExportFormat::List Document::exportFormats() const
2921 {
2922     if (!d->m_generator)
2923         return ExportFormat::List();
2924 
2925     d->cacheExportFormats();
2926     return d->m_exportFormats;
2927 }
2928 
exportTo(const QString & fileName,const ExportFormat & format) const2929 bool Document::exportTo(const QString &fileName, const ExportFormat &format) const
2930 {
2931     return d->m_generator ? d->m_generator->exportTo(fileName, format) : false;
2932 }
2933 
historyAtBegin() const2934 bool Document::historyAtBegin() const
2935 {
2936     return d->m_viewportIterator == d->m_viewportHistory.begin();
2937 }
2938 
historyAtEnd() const2939 bool Document::historyAtEnd() const
2940 {
2941     return d->m_viewportIterator == --(d->m_viewportHistory.end());
2942 }
2943 
metaData(const QString & key,const QVariant & option) const2944 QVariant Document::metaData(const QString &key, const QVariant &option) const
2945 {
2946     // if option starts with "src:" assume that we are handling a
2947     // source reference
2948     if (key == QLatin1String("NamedViewport") && option.toString().startsWith(QLatin1String("src:"), Qt::CaseInsensitive) && d->m_synctex_scanner) {
2949         const QString reference = option.toString();
2950 
2951         // The reference is of form "src:1111Filename", where "1111"
2952         // points to line number 1111 in the file "Filename".
2953         // Extract the file name and the numeral part from the reference string.
2954         // This will fail if Filename starts with a digit.
2955         QString name, lineString;
2956         // Remove "src:". Presence of substring has been checked before this
2957         // function is called.
2958         name = reference.mid(4);
2959         // split
2960         int nameLength = name.length();
2961         int i = 0;
2962         for (i = 0; i < nameLength; ++i) {
2963             if (!name[i].isDigit())
2964                 break;
2965         }
2966         lineString = name.left(i);
2967         name = name.mid(i);
2968         // Remove spaces.
2969         name = name.trimmed();
2970         lineString = lineString.trimmed();
2971         // Convert line to integer.
2972         bool ok;
2973         int line = lineString.toInt(&ok);
2974         if (!ok)
2975             line = -1;
2976 
2977         // Use column == -1 for now.
2978         if (synctex_display_query(d->m_synctex_scanner, QFile::encodeName(name).constData(), line, -1, 0) > 0) {
2979             synctex_node_p node;
2980             // For now use the first hit. Could possibly be made smarter
2981             // in case there are multiple hits.
2982             while ((node = synctex_scanner_next_result(d->m_synctex_scanner))) {
2983                 Okular::DocumentViewport viewport;
2984 
2985                 // TeX pages start at 1.
2986                 viewport.pageNumber = synctex_node_page(node) - 1;
2987 
2988                 if (viewport.pageNumber >= 0) {
2989                     const QSizeF dpi = d->m_generator->dpi();
2990 
2991                     // TeX small points ...
2992                     double px = (synctex_node_visible_h(node) * dpi.width()) / 72.27;
2993                     double py = (synctex_node_visible_v(node) * dpi.height()) / 72.27;
2994                     viewport.rePos.normalizedX = px / page(viewport.pageNumber)->width();
2995                     viewport.rePos.normalizedY = (py + 0.5) / page(viewport.pageNumber)->height();
2996                     viewport.rePos.enabled = true;
2997                     viewport.rePos.pos = Okular::DocumentViewport::Center;
2998 
2999                     return viewport.toString();
3000                 }
3001             }
3002         }
3003     }
3004     return d->m_generator ? d->m_generator->metaData(key, option) : QVariant();
3005 }
3006 
rotation() const3007 Rotation Document::rotation() const
3008 {
3009     return d->m_rotation;
3010 }
3011 
allPagesSize() const3012 QSizeF Document::allPagesSize() const
3013 {
3014     bool allPagesSameSize = true;
3015     QSizeF size;
3016     for (int i = 0; allPagesSameSize && i < d->m_pagesVector.count(); ++i) {
3017         const Page *p = d->m_pagesVector.at(i);
3018         if (i == 0)
3019             size = QSizeF(p->width(), p->height());
3020         else {
3021             allPagesSameSize = (size == QSizeF(p->width(), p->height()));
3022         }
3023     }
3024     if (allPagesSameSize)
3025         return size;
3026     else
3027         return QSizeF();
3028 }
3029 
pageSizeString(int page) const3030 QString Document::pageSizeString(int page) const
3031 {
3032     if (d->m_generator) {
3033         if (d->m_generator->pagesSizeMetric() != Generator::None) {
3034             const Page *p = d->m_pagesVector.at(page);
3035             return d->localizedSize(QSizeF(p->width(), p->height()));
3036         }
3037     }
3038     return QString();
3039 }
3040 
shouldCancelRenderingBecauseOf(const PixmapRequest & executingRequest,const PixmapRequest & otherRequest)3041 static bool shouldCancelRenderingBecauseOf(const PixmapRequest &executingRequest, const PixmapRequest &otherRequest)
3042 {
3043     // New request has higher priority -> cancel
3044     if (executingRequest.priority() > otherRequest.priority())
3045         return true;
3046 
3047     // New request has lower priority -> don't cancel
3048     if (executingRequest.priority() < otherRequest.priority())
3049         return false;
3050 
3051     // New request has same priority and is from a different observer -> don't cancel
3052     // AFAIK this never happens since all observers have different priorities
3053     if (executingRequest.observer() != otherRequest.observer())
3054         return false;
3055 
3056     // Same priority and observer, different page number -> don't cancel
3057     // may still end up cancelled later in the parent caller if none of the requests
3058     // is of the executingRequest page and RemoveAllPrevious is specified
3059     if (executingRequest.pageNumber() != otherRequest.pageNumber())
3060         return false;
3061 
3062     // Same priority, observer, page, different size -> cancel
3063     if (executingRequest.width() != otherRequest.width())
3064         return true;
3065 
3066     // Same priority, observer, page, different size -> cancel
3067     if (executingRequest.height() != otherRequest.height())
3068         return true;
3069 
3070     // Same priority, observer, page, different tiling -> cancel
3071     if (executingRequest.isTile() != otherRequest.isTile())
3072         return true;
3073 
3074     // Same priority, observer, page, different tiling -> cancel
3075     if (executingRequest.isTile()) {
3076         const NormalizedRect bothRequestsRect = executingRequest.normalizedRect() | otherRequest.normalizedRect();
3077         if (!(bothRequestsRect == executingRequest.normalizedRect()))
3078             return true;
3079     }
3080 
3081     return false;
3082 }
3083 
cancelRenderingBecauseOf(PixmapRequest * executingRequest,PixmapRequest * newRequest)3084 bool DocumentPrivate::cancelRenderingBecauseOf(PixmapRequest *executingRequest, PixmapRequest *newRequest)
3085 {
3086     // No point in aborting the rendering already finished, let it go through
3087     if (!executingRequest->d->mResultImage.isNull())
3088         return false;
3089 
3090     if (newRequest && newRequest->asynchronous() && executingRequest->partialUpdatesWanted()) {
3091         newRequest->setPartialUpdatesWanted(true);
3092     }
3093 
3094     TilesManager *tm = executingRequest->d->tilesManager();
3095     if (tm) {
3096         tm->setPixmap(nullptr, executingRequest->normalizedRect(), true /*isPartialPixmap*/);
3097         tm->setRequest(NormalizedRect(), 0, 0);
3098     }
3099     PagePrivate::PixmapObject object = executingRequest->page()->d->m_pixmaps.take(executingRequest->observer());
3100     delete object.m_pixmap;
3101 
3102     if (executingRequest->d->mShouldAbortRender != 0)
3103         return false;
3104 
3105     executingRequest->d->mShouldAbortRender = 1;
3106 
3107     if (m_generator->d_ptr->mTextPageGenerationThread && m_generator->d_ptr->mTextPageGenerationThread->page() == executingRequest->page()) {
3108         m_generator->d_ptr->mTextPageGenerationThread->abortExtraction();
3109     }
3110 
3111     return true;
3112 }
3113 
requestPixmaps(const QLinkedList<PixmapRequest * > & requests)3114 void Document::requestPixmaps(const QLinkedList<PixmapRequest *> &requests)
3115 {
3116     requestPixmaps(requests, RemoveAllPrevious);
3117 }
3118 
requestPixmaps(const QLinkedList<PixmapRequest * > & requests,PixmapRequestFlags reqOptions)3119 void Document::requestPixmaps(const QLinkedList<PixmapRequest *> &requests, PixmapRequestFlags reqOptions)
3120 {
3121     if (requests.isEmpty())
3122         return;
3123 
3124     if (!d->m_pageController) {
3125         // delete requests..
3126         QLinkedList<PixmapRequest *>::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd();
3127         for (; rIt != rEnd; ++rIt)
3128             delete *rIt;
3129         // ..and return
3130         return;
3131     }
3132 
3133     QSet<DocumentObserver *> observersPixmapCleared;
3134 
3135     // 1. [CLEAN STACK] remove previous requests of requesterID
3136     DocumentObserver *requesterObserver = requests.first()->observer();
3137     QSet<int> requestedPages;
3138     {
3139         QLinkedList<PixmapRequest *>::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd();
3140         for (; rIt != rEnd; ++rIt) {
3141             Q_ASSERT((*rIt)->observer() == requesterObserver);
3142             requestedPages.insert((*rIt)->pageNumber());
3143         }
3144     }
3145     const bool removeAllPrevious = reqOptions & RemoveAllPrevious;
3146     d->m_pixmapRequestsMutex.lock();
3147     QLinkedList<PixmapRequest *>::iterator sIt = d->m_pixmapRequestsStack.begin(), sEnd = d->m_pixmapRequestsStack.end();
3148     while (sIt != sEnd) {
3149         if ((*sIt)->observer() == requesterObserver && (removeAllPrevious || requestedPages.contains((*sIt)->pageNumber()))) {
3150             // delete request and remove it from stack
3151             delete *sIt;
3152             sIt = d->m_pixmapRequestsStack.erase(sIt);
3153         } else
3154             ++sIt;
3155     }
3156 
3157     // 1.B [PREPROCESS REQUESTS] tweak some values of the requests
3158     for (PixmapRequest *request : requests) {
3159         // set the 'page field' (see PixmapRequest) and check if it is valid
3160         qCDebug(OkularCoreDebug).nospace() << "request observer=" << request->observer() << " " << request->width() << "x" << request->height() << "@" << request->pageNumber();
3161         if (d->m_pagesVector.value(request->pageNumber()) == nullptr) {
3162             // skip requests referencing an invalid page (must not happen)
3163             delete request;
3164             continue;
3165         }
3166 
3167         request->d->mPage = d->m_pagesVector.value(request->pageNumber());
3168 
3169         if (request->isTile()) {
3170             // Change the current request rect so that only invalid tiles are
3171             // requested. Also make sure the rect is tile-aligned.
3172             NormalizedRect tilesRect;
3173             const QList<Tile> tiles = request->d->tilesManager()->tilesAt(request->normalizedRect(), TilesManager::TerminalTile);
3174             QList<Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
3175             while (tIt != tEnd) {
3176                 const Tile &tile = *tIt;
3177                 if (!tile.isValid()) {
3178                     if (tilesRect.isNull())
3179                         tilesRect = tile.rect();
3180                     else
3181                         tilesRect |= tile.rect();
3182                 }
3183 
3184                 tIt++;
3185             }
3186 
3187             request->setNormalizedRect(tilesRect);
3188         }
3189 
3190         if (!request->asynchronous())
3191             request->d->mPriority = 0;
3192     }
3193 
3194     // 1.C [CANCEL REQUESTS] cancel those requests that are running and should be cancelled because of the new requests coming in
3195     if (d->m_generator->hasFeature(Generator::SupportsCancelling)) {
3196         for (PixmapRequest *executingRequest : qAsConst(d->m_executingPixmapRequests)) {
3197             bool newRequestsContainExecutingRequestPage = false;
3198             bool requestCancelled = false;
3199             for (PixmapRequest *newRequest : requests) {
3200                 if (newRequest->pageNumber() == executingRequest->pageNumber() && requesterObserver == executingRequest->observer()) {
3201                     newRequestsContainExecutingRequestPage = true;
3202                 }
3203 
3204                 if (shouldCancelRenderingBecauseOf(*executingRequest, *newRequest)) {
3205                     requestCancelled = d->cancelRenderingBecauseOf(executingRequest, newRequest);
3206                 }
3207             }
3208 
3209             // If we were told to remove all the previous requests and the executing request page is not part of the new requests, cancel it
3210             if (!requestCancelled && removeAllPrevious && requesterObserver == executingRequest->observer() && !newRequestsContainExecutingRequestPage) {
3211                 requestCancelled = d->cancelRenderingBecauseOf(executingRequest, nullptr);
3212             }
3213 
3214             if (requestCancelled) {
3215                 observersPixmapCleared << executingRequest->observer();
3216             }
3217         }
3218     }
3219 
3220     // 2. [ADD TO STACK] add requests to stack
3221     for (PixmapRequest *request : requests) {
3222         // add request to the 'stack' at the right place
3223         if (!request->priority())
3224             // add priority zero requests to the top of the stack
3225             d->m_pixmapRequestsStack.append(request);
3226         else {
3227             // insert in stack sorted by priority
3228             sIt = d->m_pixmapRequestsStack.begin();
3229             sEnd = d->m_pixmapRequestsStack.end();
3230             while (sIt != sEnd && (*sIt)->priority() > request->priority())
3231                 ++sIt;
3232             d->m_pixmapRequestsStack.insert(sIt, request);
3233         }
3234     }
3235     d->m_pixmapRequestsMutex.unlock();
3236 
3237     // 3. [START FIRST GENERATION] if <NO>generator is ready, start a new generation,
3238     // or else (if gen is running) it will be started when the new contents will
3239     // come from generator (in requestDone())</NO>
3240     // all handling of requests put into sendGeneratorPixmapRequest
3241     //    if ( generator->canRequestPixmap() )
3242     d->sendGeneratorPixmapRequest();
3243 
3244     for (DocumentObserver *o : qAsConst(observersPixmapCleared))
3245         o->notifyContentsCleared(Okular::DocumentObserver::Pixmap);
3246 }
3247 
requestTextPage(uint pageNumber)3248 void Document::requestTextPage(uint pageNumber)
3249 {
3250     Page *kp = d->m_pagesVector[pageNumber];
3251     if (!d->m_generator || !kp)
3252         return;
3253 
3254     // Memory management for TextPages
3255 
3256     d->m_generator->generateTextPage(kp);
3257 }
3258 
notifyAnnotationChanges(int page)3259 void DocumentPrivate::notifyAnnotationChanges(int page)
3260 {
3261     foreachObserverD(notifyPageChanged(page, DocumentObserver::Annotations));
3262 }
3263 
notifyFormChanges(int)3264 void DocumentPrivate::notifyFormChanges(int /*page*/)
3265 {
3266     recalculateForms();
3267 }
3268 
addPageAnnotation(int page,Annotation * annotation)3269 void Document::addPageAnnotation(int page, Annotation *annotation)
3270 {
3271     // Transform annotation's base boundary rectangle into unrotated coordinates
3272     Page *p = d->m_pagesVector[page];
3273     QTransform t = p->d->rotationMatrix();
3274     annotation->d_ptr->baseTransform(t.inverted());
3275     QUndoCommand *uc = new AddAnnotationCommand(this->d, annotation, page);
3276     d->m_undoStack->push(uc);
3277 }
3278 
canModifyPageAnnotation(const Annotation * annotation) const3279 bool Document::canModifyPageAnnotation(const Annotation *annotation) const
3280 {
3281     if (!annotation || (annotation->flags() & Annotation::DenyWrite))
3282         return false;
3283 
3284     if (!isAllowed(Okular::AllowNotes))
3285         return false;
3286 
3287     if ((annotation->flags() & Annotation::External) && !d->canModifyExternalAnnotations())
3288         return false;
3289 
3290     switch (annotation->subType()) {
3291     case Annotation::AText:
3292     case Annotation::ALine:
3293     case Annotation::AGeom:
3294     case Annotation::AHighlight:
3295     case Annotation::AStamp:
3296     case Annotation::AInk:
3297         return true;
3298     default:
3299         return false;
3300     }
3301 }
3302 
prepareToModifyAnnotationProperties(Annotation * annotation)3303 void Document::prepareToModifyAnnotationProperties(Annotation *annotation)
3304 {
3305     Q_ASSERT(d->m_prevPropsOfAnnotBeingModified.isNull());
3306     if (!d->m_prevPropsOfAnnotBeingModified.isNull()) {
3307         qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties has already been called since last call to Document::modifyPageAnnotationProperties";
3308         return;
3309     }
3310     d->m_prevPropsOfAnnotBeingModified = annotation->getAnnotationPropertiesDomNode();
3311 }
3312 
modifyPageAnnotationProperties(int page,Annotation * annotation)3313 void Document::modifyPageAnnotationProperties(int page, Annotation *annotation)
3314 {
3315     Q_ASSERT(!d->m_prevPropsOfAnnotBeingModified.isNull());
3316     if (d->m_prevPropsOfAnnotBeingModified.isNull()) {
3317         qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties must be called before Annotation is modified";
3318         return;
3319     }
3320     QDomNode prevProps = d->m_prevPropsOfAnnotBeingModified;
3321     QUndoCommand *uc = new Okular::ModifyAnnotationPropertiesCommand(d, annotation, page, prevProps, annotation->getAnnotationPropertiesDomNode());
3322     d->m_undoStack->push(uc);
3323     d->m_prevPropsOfAnnotBeingModified.clear();
3324 }
3325 
translatePageAnnotation(int page,Annotation * annotation,const NormalizedPoint & delta)3326 void Document::translatePageAnnotation(int page, Annotation *annotation, const NormalizedPoint &delta)
3327 {
3328     int complete = (annotation->flags() & Okular::Annotation::BeingMoved) == 0;
3329     QUndoCommand *uc = new Okular::TranslateAnnotationCommand(d, annotation, page, delta, complete);
3330     d->m_undoStack->push(uc);
3331 }
3332 
adjustPageAnnotation(int page,Annotation * annotation,const Okular::NormalizedPoint & delta1,const Okular::NormalizedPoint & delta2)3333 void Document::adjustPageAnnotation(int page, Annotation *annotation, const Okular::NormalizedPoint &delta1, const Okular::NormalizedPoint &delta2)
3334 {
3335     const bool complete = (annotation->flags() & Okular::Annotation::BeingResized) == 0;
3336     QUndoCommand *uc = new Okular::AdjustAnnotationCommand(d, annotation, page, delta1, delta2, complete);
3337     d->m_undoStack->push(uc);
3338 }
3339 
editPageAnnotationContents(int page,Annotation * annotation,const QString & newContents,int newCursorPos,int prevCursorPos,int prevAnchorPos)3340 void Document::editPageAnnotationContents(int page, Annotation *annotation, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3341 {
3342     QString prevContents = annotation->contents();
3343     QUndoCommand *uc = new EditAnnotationContentsCommand(d, annotation, page, newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos);
3344     d->m_undoStack->push(uc);
3345 }
3346 
canRemovePageAnnotation(const Annotation * annotation) const3347 bool Document::canRemovePageAnnotation(const Annotation *annotation) const
3348 {
3349     if (!annotation || (annotation->flags() & Annotation::DenyDelete))
3350         return false;
3351 
3352     if ((annotation->flags() & Annotation::External) && !d->canRemoveExternalAnnotations())
3353         return false;
3354 
3355     switch (annotation->subType()) {
3356     case Annotation::AText:
3357     case Annotation::ALine:
3358     case Annotation::AGeom:
3359     case Annotation::AHighlight:
3360     case Annotation::AStamp:
3361     case Annotation::AInk:
3362     case Annotation::ACaret:
3363         return true;
3364     default:
3365         return false;
3366     }
3367 }
3368 
removePageAnnotation(int page,Annotation * annotation)3369 void Document::removePageAnnotation(int page, Annotation *annotation)
3370 {
3371     QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page);
3372     d->m_undoStack->push(uc);
3373 }
3374 
removePageAnnotations(int page,const QList<Annotation * > & annotations)3375 void Document::removePageAnnotations(int page, const QList<Annotation *> &annotations)
3376 {
3377     d->m_undoStack->beginMacro(i18nc("remove a collection of annotations from the page", "remove annotations"));
3378     foreach (Annotation *annotation, annotations) {
3379         QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page);
3380         d->m_undoStack->push(uc);
3381     }
3382     d->m_undoStack->endMacro();
3383 }
3384 
canAddAnnotationsNatively() const3385 bool DocumentPrivate::canAddAnnotationsNatively() const
3386 {
3387     Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3388 
3389     if (iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Addition))
3390         return true;
3391 
3392     return false;
3393 }
3394 
canModifyExternalAnnotations() const3395 bool DocumentPrivate::canModifyExternalAnnotations() const
3396 {
3397     Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3398 
3399     if (iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Modification))
3400         return true;
3401 
3402     return false;
3403 }
3404 
canRemoveExternalAnnotations() const3405 bool DocumentPrivate::canRemoveExternalAnnotations() const
3406 {
3407     Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3408 
3409     if (iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Removal))
3410         return true;
3411 
3412     return false;
3413 }
3414 
setPageTextSelection(int page,RegularAreaRect * rect,const QColor & color)3415 void Document::setPageTextSelection(int page, RegularAreaRect *rect, const QColor &color)
3416 {
3417     Page *kp = d->m_pagesVector[page];
3418     if (!d->m_generator || !kp)
3419         return;
3420 
3421     // add or remove the selection basing whether rect is null or not
3422     if (rect)
3423         kp->d->setTextSelections(rect, color);
3424     else
3425         kp->d->deleteTextSelections();
3426 
3427     // notify observers about the change
3428     foreachObserver(notifyPageChanged(page, DocumentObserver::TextSelection));
3429 }
3430 
canUndo() const3431 bool Document::canUndo() const
3432 {
3433     return d->m_undoStack->canUndo();
3434 }
3435 
canRedo() const3436 bool Document::canRedo() const
3437 {
3438     return d->m_undoStack->canRedo();
3439 }
3440 
3441 /* REFERENCE IMPLEMENTATION: better calling setViewport from other code
3442 void Document::setNextPage()
3443 {
3444     // advance page and set viewport on observers
3445     if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 )
3446         setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber + 1 ) );
3447 }
3448 
3449 void Document::setPrevPage()
3450 {
3451     // go to previous page and set viewport on observers
3452     if ( (*d->m_viewportIterator).pageNumber > 0 )
3453         setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber - 1 ) );
3454 }
3455 */
3456 
setViewportWithHistory(const DocumentViewport & viewport,DocumentObserver * excludeObserver,bool smoothMove,bool updateHistory)3457 void Document::setViewportWithHistory(const DocumentViewport &viewport, DocumentObserver *excludeObserver, bool smoothMove, bool updateHistory)
3458 {
3459     if (!viewport.isValid()) {
3460         qCDebug(OkularCoreDebug) << "invalid viewport:" << viewport.toString();
3461         return;
3462     }
3463     if (viewport.pageNumber >= int(d->m_pagesVector.count())) {
3464         // qCDebug(OkularCoreDebug) << "viewport out of document:" << viewport.toString();
3465         return;
3466     }
3467 
3468     // if already broadcasted, don't redo it
3469     DocumentViewport &oldViewport = *d->m_viewportIterator;
3470     // disabled by enrico on 2005-03-18 (less debug output)
3471     // if ( viewport == oldViewport )
3472     //    qCDebug(OkularCoreDebug) << "setViewport with the same viewport.";
3473 
3474     const int oldPageNumber = oldViewport.pageNumber;
3475 
3476     // set internal viewport taking care of history
3477     if (oldViewport.pageNumber == viewport.pageNumber || !oldViewport.isValid() || !updateHistory) {
3478         // if page is unchanged save the viewport at current position in queue
3479         oldViewport = viewport;
3480     } else {
3481         // remove elements after viewportIterator in queue
3482         d->m_viewportHistory.erase(++d->m_viewportIterator, d->m_viewportHistory.end());
3483 
3484         // keep the list to a reasonable size by removing head when needed
3485         if (d->m_viewportHistory.count() >= OKULAR_HISTORY_MAXSTEPS)
3486             d->m_viewportHistory.pop_front();
3487 
3488         // add the item at the end of the queue
3489         d->m_viewportIterator = d->m_viewportHistory.insert(d->m_viewportHistory.end(), viewport);
3490     }
3491 
3492     const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3493 
3494     const bool currentPageChanged = (oldPageNumber != currentViewportPage);
3495 
3496     // notify change to all other (different from id) observers
3497     for (DocumentObserver *o : qAsConst(d->m_observers)) {
3498         if (o != excludeObserver)
3499             o->notifyViewportChanged(smoothMove);
3500 
3501         if (currentPageChanged)
3502             o->notifyCurrentPageChanged(oldPageNumber, currentViewportPage);
3503     }
3504 }
3505 
setViewportPage(int page,DocumentObserver * excludeObserver,bool smoothMove)3506 void Document::setViewportPage(int page, DocumentObserver *excludeObserver, bool smoothMove)
3507 {
3508     // clamp page in range [0 ... numPages-1]
3509     if (page < 0)
3510         page = 0;
3511     else if (page > (int)d->m_pagesVector.count())
3512         page = d->m_pagesVector.count() - 1;
3513 
3514     // make a viewport from the page and broadcast it
3515     setViewport(DocumentViewport(page), excludeObserver, smoothMove);
3516 }
3517 
setViewport(const DocumentViewport & viewport,DocumentObserver * excludeObserver,bool smoothMove)3518 void Document::setViewport(const DocumentViewport &viewport, DocumentObserver *excludeObserver, bool smoothMove)
3519 {
3520     // set viewport, updating history
3521     setViewportWithHistory(viewport, excludeObserver, smoothMove, true);
3522 }
3523 
setZoom(int factor,DocumentObserver * excludeObserver)3524 void Document::setZoom(int factor, DocumentObserver *excludeObserver)
3525 {
3526     // notify change to all other (different from id) observers
3527     for (DocumentObserver *o : qAsConst(d->m_observers))
3528         if (o != excludeObserver)
3529             o->notifyZoom(factor);
3530 }
3531 
setPrevViewport()3532 void Document::setPrevViewport()
3533 // restore viewport from the history
3534 {
3535     if (d->m_viewportIterator != d->m_viewportHistory.begin()) {
3536         const int oldViewportPage = (*d->m_viewportIterator).pageNumber;
3537 
3538         // restore previous viewport and notify it to observers
3539         --d->m_viewportIterator;
3540         foreachObserver(notifyViewportChanged(true));
3541 
3542         const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3543         if (oldViewportPage != currentViewportPage)
3544             foreachObserver(notifyCurrentPageChanged(oldViewportPage, currentViewportPage));
3545     }
3546 }
3547 
setNextViewport()3548 void Document::setNextViewport()
3549 // restore next viewport from the history
3550 {
3551     auto nextIterator = QLinkedList<DocumentViewport>::const_iterator(d->m_viewportIterator);
3552     ++nextIterator;
3553     if (nextIterator != d->m_viewportHistory.constEnd()) {
3554         const int oldViewportPage = (*d->m_viewportIterator).pageNumber;
3555 
3556         // restore next viewport and notify it to observers
3557         ++d->m_viewportIterator;
3558         foreachObserver(notifyViewportChanged(true));
3559 
3560         const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3561         if (oldViewportPage != currentViewportPage)
3562             foreachObserver(notifyCurrentPageChanged(oldViewportPage, currentViewportPage));
3563     }
3564 }
3565 
setNextDocumentViewport(const DocumentViewport & viewport)3566 void Document::setNextDocumentViewport(const DocumentViewport &viewport)
3567 {
3568     d->m_nextDocumentViewport = viewport;
3569 }
3570 
setNextDocumentDestination(const QString & namedDestination)3571 void Document::setNextDocumentDestination(const QString &namedDestination)
3572 {
3573     d->m_nextDocumentDestination = namedDestination;
3574 }
3575 
searchText(int searchID,const QString & text,bool fromStart,Qt::CaseSensitivity caseSensitivity,SearchType type,bool moveViewport,const QColor & color)3576 void Document::searchText(int searchID, const QString &text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor &color)
3577 {
3578     d->m_searchCancelled = false;
3579 
3580     // safety checks: don't perform searches on empty or unsearchable docs
3581     if (!d->m_generator || !d->m_generator->hasFeature(Generator::TextExtraction) || d->m_pagesVector.isEmpty()) {
3582         emit searchFinished(searchID, NoMatchFound);
3583         return;
3584     }
3585 
3586     // if searchID search not recorded, create new descriptor and init params
3587     QMap<int, RunningSearch *>::iterator searchIt = d->m_searches.find(searchID);
3588     if (searchIt == d->m_searches.end()) {
3589         RunningSearch *search = new RunningSearch();
3590         search->continueOnPage = -1;
3591         searchIt = d->m_searches.insert(searchID, search);
3592     }
3593     RunningSearch *s = *searchIt;
3594 
3595     // update search structure
3596     bool newText = text != s->cachedString;
3597     s->cachedString = text;
3598     s->cachedType = type;
3599     s->cachedCaseSensitivity = caseSensitivity;
3600     s->cachedViewportMove = moveViewport;
3601     s->cachedColor = color;
3602     s->isCurrentlySearching = true;
3603 
3604     // global data for search
3605     QSet<int> *pagesToNotify = new QSet<int>;
3606 
3607     // remove highlights from pages and queue them for notifying changes
3608     *pagesToNotify += s->highlightedPages;
3609     for (const int pageNumber : qAsConst(s->highlightedPages)) {
3610         d->m_pagesVector.at(pageNumber)->d->deleteHighlights(searchID);
3611     }
3612     s->highlightedPages.clear();
3613 
3614     // set hourglass cursor
3615     QApplication::setOverrideCursor(Qt::WaitCursor);
3616 
3617     // 1. ALLDOC - process all document marking pages
3618     if (type == AllDocument) {
3619         QMap<Page *, QVector<RegularAreaRect *>> *pageMatches = new QMap<Page *, QVector<RegularAreaRect *>>;
3620 
3621         // search and highlight 'text' (as a solid phrase) on all pages
3622         QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID] { d->doContinueAllDocumentSearch(pagesToNotify, pageMatches, 0, searchID); });
3623     }
3624     // 2. NEXTMATCH - find next matching item (or start from top)
3625     // 3. PREVMATCH - find previous matching item (or start from bottom)
3626     else if (type == NextMatch || type == PreviousMatch) {
3627         // find out from where to start/resume search from
3628         const bool forward = type == NextMatch;
3629         const int viewportPage = (*d->m_viewportIterator).pageNumber;
3630         const int fromStartSearchPage = forward ? 0 : d->m_pagesVector.count() - 1;
3631         int currentPage = fromStart ? fromStartSearchPage : ((s->continueOnPage != -1) ? s->continueOnPage : viewportPage);
3632         Page *lastPage = fromStart ? nullptr : d->m_pagesVector[currentPage];
3633         int pagesDone = 0;
3634 
3635         // continue checking last TextPage first (if it is the current page)
3636         RegularAreaRect *match = nullptr;
3637         if (lastPage && lastPage->number() == s->continueOnPage) {
3638             if (newText)
3639                 match = lastPage->findText(searchID, text, forward ? FromTop : FromBottom, caseSensitivity);
3640             else
3641                 match = lastPage->findText(searchID, text, forward ? NextResult : PreviousResult, caseSensitivity, &s->continueOnMatch);
3642             if (!match) {
3643                 if (forward)
3644                     currentPage++;
3645                 else
3646                     currentPage--;
3647                 pagesDone++;
3648             }
3649         }
3650 
3651         s->pagesDone = pagesDone;
3652 
3653         DoContinueDirectionMatchSearchStruct *searchStruct = new DoContinueDirectionMatchSearchStruct();
3654         searchStruct->pagesToNotify = pagesToNotify;
3655         searchStruct->match = match;
3656         searchStruct->currentPage = currentPage;
3657         searchStruct->searchID = searchID;
3658 
3659         QTimer::singleShot(0, this, [this, searchStruct] { d->doContinueDirectionMatchSearch(searchStruct); });
3660     }
3661     // 4. GOOGLE* - process all document marking pages
3662     else if (type == GoogleAll || type == GoogleAny) {
3663         QMap<Page *, QVector<QPair<RegularAreaRect *, QColor>>> *pageMatches = new QMap<Page *, QVector<QPair<RegularAreaRect *, QColor>>>;
3664         const QStringList words = text.split(QLatin1Char(' '), QString::SkipEmptyParts);
3665 
3666         // search and highlight every word in 'text' on all pages
3667         QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID, words] { d->doContinueGooglesDocumentSearch(pagesToNotify, pageMatches, 0, searchID, words); });
3668     }
3669 }
3670 
continueSearch(int searchID)3671 void Document::continueSearch(int searchID)
3672 {
3673     // check if searchID is present in runningSearches
3674     QMap<int, RunningSearch *>::const_iterator it = d->m_searches.constFind(searchID);
3675     if (it == d->m_searches.constEnd()) {
3676         emit searchFinished(searchID, NoMatchFound);
3677         return;
3678     }
3679 
3680     // start search with cached parameters from last search by searchID
3681     RunningSearch *p = *it;
3682     if (!p->isCurrentlySearching)
3683         searchText(searchID, p->cachedString, false, p->cachedCaseSensitivity, p->cachedType, p->cachedViewportMove, p->cachedColor);
3684 }
3685 
continueSearch(int searchID,SearchType type)3686 void Document::continueSearch(int searchID, SearchType type)
3687 {
3688     // check if searchID is present in runningSearches
3689     QMap<int, RunningSearch *>::const_iterator it = d->m_searches.constFind(searchID);
3690     if (it == d->m_searches.constEnd()) {
3691         emit searchFinished(searchID, NoMatchFound);
3692         return;
3693     }
3694 
3695     // start search with cached parameters from last search by searchID
3696     RunningSearch *p = *it;
3697     if (!p->isCurrentlySearching)
3698         searchText(searchID, p->cachedString, false, p->cachedCaseSensitivity, type, p->cachedViewportMove, p->cachedColor);
3699 }
3700 
resetSearch(int searchID)3701 void Document::resetSearch(int searchID)
3702 {
3703     // if we are closing down, don't bother doing anything
3704     if (!d->m_generator)
3705         return;
3706 
3707     // check if searchID is present in runningSearches
3708     QMap<int, RunningSearch *>::iterator searchIt = d->m_searches.find(searchID);
3709     if (searchIt == d->m_searches.end())
3710         return;
3711 
3712     // get previous parameters for search
3713     RunningSearch *s = *searchIt;
3714 
3715     // unhighlight pages and inform observers about that
3716     for (const int pageNumber : qAsConst(s->highlightedPages)) {
3717         d->m_pagesVector.at(pageNumber)->d->deleteHighlights(searchID);
3718         foreachObserver(notifyPageChanged(pageNumber, DocumentObserver::Highlights));
3719     }
3720 
3721     // send the setup signal too (to update views that filter on matches)
3722     foreachObserver(notifySetup(d->m_pagesVector, 0));
3723 
3724     // remove search from the runningSearches list and delete it
3725     d->m_searches.erase(searchIt);
3726     delete s;
3727 }
3728 
cancelSearch()3729 void Document::cancelSearch()
3730 {
3731     d->m_searchCancelled = true;
3732 }
3733 
undo()3734 void Document::undo()
3735 {
3736     d->m_undoStack->undo();
3737 }
3738 
redo()3739 void Document::redo()
3740 {
3741     d->m_undoStack->redo();
3742 }
3743 
editFormText(int pageNumber,Okular::FormFieldText * form,const QString & newContents,int newCursorPos,int prevCursorPos,int prevAnchorPos)3744 void Document::editFormText(int pageNumber, Okular::FormFieldText *form, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3745 {
3746     QUndoCommand *uc = new EditFormTextCommand(this->d, form, pageNumber, newContents, newCursorPos, form->text(), prevCursorPos, prevAnchorPos);
3747     d->m_undoStack->push(uc);
3748 }
3749 
editFormList(int pageNumber,FormFieldChoice * form,const QList<int> & newChoices)3750 void Document::editFormList(int pageNumber, FormFieldChoice *form, const QList<int> &newChoices)
3751 {
3752     const QList<int> prevChoices = form->currentChoices();
3753     QUndoCommand *uc = new EditFormListCommand(this->d, form, pageNumber, newChoices, prevChoices);
3754     d->m_undoStack->push(uc);
3755 }
3756 
editFormCombo(int pageNumber,FormFieldChoice * form,const QString & newText,int newCursorPos,int prevCursorPos,int prevAnchorPos)3757 void Document::editFormCombo(int pageNumber, FormFieldChoice *form, const QString &newText, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3758 {
3759     QString prevText;
3760     if (form->currentChoices().isEmpty()) {
3761         prevText = form->editChoice();
3762     } else {
3763         prevText = form->choices().at(form->currentChoices().constFirst());
3764     }
3765 
3766     QUndoCommand *uc = new EditFormComboCommand(this->d, form, pageNumber, newText, newCursorPos, prevText, prevCursorPos, prevAnchorPos);
3767     d->m_undoStack->push(uc);
3768 }
3769 
editFormButtons(int pageNumber,const QList<FormFieldButton * > & formButtons,const QList<bool> & newButtonStates)3770 void Document::editFormButtons(int pageNumber, const QList<FormFieldButton *> &formButtons, const QList<bool> &newButtonStates)
3771 {
3772     QUndoCommand *uc = new EditFormButtonsCommand(this->d, pageNumber, formButtons, newButtonStates);
3773     d->m_undoStack->push(uc);
3774 }
3775 
reloadDocument() const3776 void Document::reloadDocument() const
3777 {
3778     const int numOfPages = pages();
3779     for (int i = currentPage(); i >= 0; i--)
3780         d->refreshPixmaps(i);
3781     for (int i = currentPage() + 1; i < numOfPages; i++)
3782         d->refreshPixmaps(i);
3783 }
3784 
bookmarkManager() const3785 BookmarkManager *Document::bookmarkManager() const
3786 {
3787     return d->m_bookmarkManager;
3788 }
3789 
bookmarkedPageList() const3790 QList<int> Document::bookmarkedPageList() const
3791 {
3792     QList<int> list;
3793     uint docPages = pages();
3794 
3795     // pages are 0-indexed internally, but 1-indexed externally
3796     for (uint i = 0; i < docPages; i++) {
3797         if (bookmarkManager()->isBookmarked(i)) {
3798             list << i + 1;
3799         }
3800     }
3801     return list;
3802 }
3803 
bookmarkedPageRange() const3804 QString Document::bookmarkedPageRange() const
3805 {
3806     // Code formerly in Part::slotPrint()
3807     // range detecting
3808     QString range;
3809     uint docPages = pages();
3810     int startId = -1;
3811     int endId = -1;
3812 
3813     for (uint i = 0; i < docPages; ++i) {
3814         if (bookmarkManager()->isBookmarked(i)) {
3815             if (startId < 0)
3816                 startId = i;
3817             if (endId < 0)
3818                 endId = startId;
3819             else
3820                 ++endId;
3821         } else if (startId >= 0 && endId >= 0) {
3822             if (!range.isEmpty())
3823                 range += QLatin1Char(',');
3824 
3825             if (endId - startId > 0)
3826                 range += QStringLiteral("%1-%2").arg(startId + 1).arg(endId + 1);
3827             else
3828                 range += QString::number(startId + 1);
3829             startId = -1;
3830             endId = -1;
3831         }
3832     }
3833     if (startId >= 0 && endId >= 0) {
3834         if (!range.isEmpty())
3835             range += QLatin1Char(',');
3836 
3837         if (endId - startId > 0)
3838             range += QStringLiteral("%1-%2").arg(startId + 1).arg(endId + 1);
3839         else
3840             range += QString::number(startId + 1);
3841     }
3842     return range;
3843 }
3844 
3845 struct ExecuteNextActionsHelper : public QObject {
3846     Q_OBJECT
3847 public:
3848     bool b = true;
3849 };
3850 
processAction(const Action * action)3851 void Document::processAction(const Action *action)
3852 {
3853     if (!action)
3854         return;
3855 
3856     // Don't execute next actions if the action itself caused the closing of the document
3857     ExecuteNextActionsHelper executeNextActions;
3858     connect(this, &Document::aboutToClose, &executeNextActions, [&executeNextActions] { executeNextActions.b = false; });
3859 
3860     switch (action->actionType()) {
3861     case Action::Goto: {
3862         const GotoAction *go = static_cast<const GotoAction *>(action);
3863         d->m_nextDocumentViewport = go->destViewport();
3864         d->m_nextDocumentDestination = go->destinationName();
3865 
3866         // Explanation of why d->m_nextDocumentViewport is needed:
3867         // all openRelativeFile does is launch a signal telling we
3868         // want to open another URL, the problem is that when the file is
3869         // non local, the loading is done asynchronously so you can't
3870         // do a setViewport after the if as it was because you are doing the setViewport
3871         // on the old file and when the new arrives there is no setViewport for it and
3872         // it does not show anything
3873 
3874         // first open filename if link is pointing outside this document
3875         const QString filename = go->fileName();
3876         if (go->isExternal() && !d->openRelativeFile(filename)) {
3877             qCWarning(OkularCoreDebug).nospace() << "Action: Error opening '" << filename << "'.";
3878             break;
3879         } else {
3880             const DocumentViewport nextViewport = d->nextDocumentViewport();
3881             // skip local links that point to nowhere (broken ones)
3882             if (!nextViewport.isValid())
3883                 break;
3884 
3885             setViewport(nextViewport, nullptr, true);
3886             d->m_nextDocumentViewport = DocumentViewport();
3887             d->m_nextDocumentDestination = QString();
3888         }
3889 
3890     } break;
3891 
3892     case Action::Execute: {
3893         const ExecuteAction *exe = static_cast<const ExecuteAction *>(action);
3894         const QString fileName = exe->fileName();
3895         if (fileName.endsWith(QLatin1String(".pdf"), Qt::CaseInsensitive)) {
3896             d->openRelativeFile(fileName);
3897             break;
3898         }
3899 
3900         // Albert: the only pdf i have that has that kind of link don't define
3901         // an application and use the fileName as the file to open
3902         QUrl url = d->giveAbsoluteUrl(fileName);
3903         QMimeDatabase db;
3904         QMimeType mime = db.mimeTypeForUrl(url);
3905         // Check executables
3906         if (KRun::isExecutableFile(url, mime.name())) {
3907             // Don't have any pdf that uses this code path, just a guess on how it should work
3908             if (!exe->parameters().isEmpty()) {
3909                 url = d->giveAbsoluteUrl(exe->parameters());
3910                 mime = db.mimeTypeForUrl(url);
3911 
3912                 if (KRun::isExecutableFile(url, mime.name())) {
3913                     // this case is a link pointing to an executable with a parameter
3914                     // that also is an executable, possibly a hand-crafted pdf
3915                     emit error(i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that."), -1);
3916                     break;
3917                 }
3918             } else {
3919                 // this case is a link pointing to an executable with no parameters
3920                 // core developers find unacceptable executing it even after asking the user
3921                 emit error(i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that."), -1);
3922                 break;
3923             }
3924         }
3925 
3926         KService::Ptr ptr = KApplicationTrader::preferredService(mime.name());
3927         if (ptr) {
3928             QList<QUrl> lst;
3929             lst.append(url);
3930             KRun::runService(*ptr, lst, nullptr);
3931         } else
3932             emit error(i18n("No application found for opening file of mimetype %1.", mime.name()), -1);
3933     } break;
3934 
3935     case Action::DocAction: {
3936         const DocumentAction *docaction = static_cast<const DocumentAction *>(action);
3937         switch (docaction->documentActionType()) {
3938         case DocumentAction::PageFirst:
3939             setViewportPage(0);
3940             break;
3941         case DocumentAction::PagePrev:
3942             if ((*d->m_viewportIterator).pageNumber > 0)
3943                 setViewportPage((*d->m_viewportIterator).pageNumber - 1);
3944             break;
3945         case DocumentAction::PageNext:
3946             if ((*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1)
3947                 setViewportPage((*d->m_viewportIterator).pageNumber + 1);
3948             break;
3949         case DocumentAction::PageLast:
3950             setViewportPage(d->m_pagesVector.count() - 1);
3951             break;
3952         case DocumentAction::HistoryBack:
3953             setPrevViewport();
3954             break;
3955         case DocumentAction::HistoryForward:
3956             setNextViewport();
3957             break;
3958         case DocumentAction::Quit:
3959             emit quit();
3960             break;
3961         case DocumentAction::Presentation:
3962             emit linkPresentation();
3963             break;
3964         case DocumentAction::EndPresentation:
3965             emit linkEndPresentation();
3966             break;
3967         case DocumentAction::Find:
3968             emit linkFind();
3969             break;
3970         case DocumentAction::GoToPage:
3971             emit linkGoToPage();
3972             break;
3973         case DocumentAction::Close:
3974             emit close();
3975             break;
3976         }
3977     } break;
3978 
3979     case Action::Browse: {
3980         const BrowseAction *browse = static_cast<const BrowseAction *>(action);
3981         QString lilySource;
3982         int lilyRow = 0, lilyCol = 0;
3983         // if the url is a mailto one, invoke mailer
3984         if (browse->url().scheme() == QLatin1String("mailto")) {
3985             QDesktopServices::openUrl(browse->url());
3986         } else if (extractLilyPondSourceReference(browse->url(), &lilySource, &lilyRow, &lilyCol)) {
3987             const SourceReference ref(lilySource, lilyRow, lilyCol);
3988             processSourceReference(&ref);
3989         } else {
3990             const QUrl url = browse->url();
3991 
3992             // fix for #100366, documents with relative links that are the form of http:foo.pdf
3993             if ((url.scheme() == QLatin1String("http")) && url.host().isEmpty() && url.fileName().endsWith(QLatin1String("pdf"))) {
3994                 d->openRelativeFile(url.fileName());
3995                 break;
3996             }
3997 
3998             // handle documents with relative path
3999             if (d->m_url.isValid()) {
4000                 const QUrl realUrl = KIO::upUrl(d->m_url).resolved(url);
4001                 // KRun autodeletes
4002                 KRun *r = new KRun(realUrl, d->m_widget);
4003                 r->setRunExecutables(false);
4004             }
4005         }
4006     } break;
4007 
4008     case Action::Sound: {
4009         const SoundAction *linksound = static_cast<const SoundAction *>(action);
4010         AudioPlayer::instance()->playSound(linksound->sound(), linksound);
4011     } break;
4012 
4013     case Action::Script: {
4014         const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4015         if (!d->m_scripter)
4016             d->m_scripter = new Scripter(d);
4017         d->m_scripter->execute(linkscript->scriptType(), linkscript->script());
4018     } break;
4019 
4020     case Action::Movie:
4021         emit processMovieAction(static_cast<const MovieAction *>(action));
4022         break;
4023     case Action::Rendition: {
4024         const RenditionAction *linkrendition = static_cast<const RenditionAction *>(action);
4025         if (!linkrendition->script().isEmpty()) {
4026             if (!d->m_scripter)
4027                 d->m_scripter = new Scripter(d);
4028             d->m_scripter->execute(linkrendition->scriptType(), linkrendition->script());
4029         }
4030 
4031         emit processRenditionAction(static_cast<const RenditionAction *>(action));
4032     } break;
4033     case Action::BackendOpaque: {
4034         d->m_generator->opaqueAction(static_cast<const BackendOpaqueAction *>(action));
4035     } break;
4036     }
4037 
4038     if (executeNextActions.b) {
4039         const QVector<Action *> nextActions = action->nextActions();
4040         for (const Action *a : nextActions) {
4041             processAction(a);
4042         }
4043     }
4044 }
4045 
processFormatAction(const Action * action,Okular::FormFieldText * fft)4046 void Document::processFormatAction(const Action *action, Okular::FormFieldText *fft)
4047 {
4048     if (action->actionType() != Action::Script) {
4049         qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for formatting.";
4050         return;
4051     }
4052 
4053     // Lookup the page of the FormFieldText
4054     int foundPage = d->findFieldPageNumber(fft);
4055 
4056     if (foundPage == -1) {
4057         qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4058         return;
4059     }
4060 
4061     const QString unformattedText = fft->text();
4062 
4063     std::shared_ptr<Event> event = Event::createFormatEvent(fft, d->m_pagesVector[foundPage]);
4064 
4065     const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4066 
4067     d->executeScriptEvent(event, linkscript);
4068 
4069     const QString formattedText = event->value().toString();
4070     if (formattedText != unformattedText) {
4071         // We set the formattedText, because when we call refreshFormWidget
4072         // It will set the QLineEdit to this formattedText
4073         fft->setText(formattedText);
4074         fft->setAppearanceText(formattedText);
4075         emit refreshFormWidget(fft);
4076         d->refreshPixmaps(foundPage);
4077         // Then we make the form have the unformatted text, to use
4078         // in calculations and other things.
4079         fft->setText(unformattedText);
4080     } else if (fft->additionalAction(FormField::CalculateField)) {
4081         // When the field was calculated we need to refresh even
4082         // if the format script changed nothing. e.g. on error.
4083         // This is because the recalculateForms function delegated
4084         // the responsiblity for the refresh to us.
4085         emit refreshFormWidget(fft);
4086         d->refreshPixmaps(foundPage);
4087     }
4088 }
4089 
processKeystrokeAction(const Action * action,Okular::FormFieldText * fft,bool & returnCode)4090 void Document::processKeystrokeAction(const Action *action, Okular::FormFieldText *fft, bool &returnCode)
4091 {
4092     if (action->actionType() != Action::Script) {
4093         qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for keystroke.";
4094         return;
4095     }
4096     // Lookup the page of the FormFieldText
4097     int foundPage = d->findFieldPageNumber(fft);
4098 
4099     if (foundPage == -1) {
4100         qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4101         return;
4102     }
4103 
4104     std::shared_ptr<Event> event = Event::createKeystrokeEvent(fft, d->m_pagesVector[foundPage]);
4105 
4106     const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4107 
4108     d->executeScriptEvent(event, linkscript);
4109 
4110     returnCode = event->returnCode();
4111 }
4112 
processFocusAction(const Action * action,Okular::FormField * field)4113 void Document::processFocusAction(const Action *action, Okular::FormField *field)
4114 {
4115     if (!action || action->actionType() != Action::Script)
4116         return;
4117 
4118     // Lookup the page of the FormFieldText
4119     int foundPage = d->findFieldPageNumber(field);
4120 
4121     if (foundPage == -1) {
4122         qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4123         return;
4124     }
4125 
4126     std::shared_ptr<Event> event = Event::createFormFocusEvent(field, d->m_pagesVector[foundPage]);
4127 
4128     const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4129 
4130     d->executeScriptEvent(event, linkscript);
4131 }
4132 
processValidateAction(const Action * action,Okular::FormFieldText * fft,bool & returnCode)4133 void Document::processValidateAction(const Action *action, Okular::FormFieldText *fft, bool &returnCode)
4134 {
4135     if (!action || action->actionType() != Action::Script)
4136         return;
4137 
4138     // Lookup the page of the FormFieldText
4139     int foundPage = d->findFieldPageNumber(fft);
4140 
4141     if (foundPage == -1) {
4142         qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4143         return;
4144     }
4145 
4146     std::shared_ptr<Event> event = Event::createFormValidateEvent(fft, d->m_pagesVector[foundPage]);
4147 
4148     const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4149 
4150     d->executeScriptEvent(event, linkscript);
4151     returnCode = event->returnCode();
4152 }
4153 
processSourceReference(const SourceReference * ref)4154 void Document::processSourceReference(const SourceReference *ref)
4155 {
4156     if (!ref)
4157         return;
4158 
4159     const QUrl url = d->giveAbsoluteUrl(ref->fileName());
4160     if (!url.isLocalFile()) {
4161         qCDebug(OkularCoreDebug) << url.url() << "is not a local file.";
4162         return;
4163     }
4164 
4165     const QString absFileName = url.toLocalFile();
4166     if (!QFile::exists(absFileName)) {
4167         qCDebug(OkularCoreDebug) << "No such file:" << absFileName;
4168         return;
4169     }
4170 
4171     bool handled = false;
4172     emit sourceReferenceActivated(absFileName, ref->row(), ref->column(), &handled);
4173     if (handled) {
4174         return;
4175     }
4176 
4177     static QHash<int, QString> editors;
4178     // init the editors table if empty (on first run, usually)
4179     if (editors.isEmpty()) {
4180         editors = buildEditorsMap();
4181     }
4182 
4183     QHash<int, QString>::const_iterator it = editors.constFind(SettingsCore::externalEditor());
4184     QString p;
4185     if (it != editors.constEnd())
4186         p = *it;
4187     else
4188         p = SettingsCore::externalEditorCommand();
4189     // custom editor not yet configured
4190     if (p.isEmpty())
4191         return;
4192 
4193     // manually append the %f placeholder if not specified
4194     if (p.indexOf(QLatin1String("%f")) == -1)
4195         p.append(QLatin1String(" %f"));
4196 
4197     // replacing the placeholders
4198     QHash<QChar, QString> map;
4199     map.insert(QLatin1Char('f'), absFileName);
4200     map.insert(QLatin1Char('c'), QString::number(ref->column()));
4201     map.insert(QLatin1Char('l'), QString::number(ref->row()));
4202     const QString cmd = KMacroExpander::expandMacrosShellQuote(p, map);
4203     if (cmd.isEmpty())
4204         return;
4205     QStringList args = KShell::splitArgs(cmd);
4206     if (args.isEmpty())
4207         return;
4208 
4209     const QString prog = args.takeFirst();
4210     // Make sure prog is in PATH and not just in the CWD
4211     const QString progFullPath = QStandardPaths::findExecutable(prog);
4212     if (progFullPath.isEmpty()) {
4213         return;
4214     }
4215 
4216     KProcess::startDetached(progFullPath, args);
4217 }
4218 
dynamicSourceReference(int pageNr,double absX,double absY)4219 const SourceReference *Document::dynamicSourceReference(int pageNr, double absX, double absY)
4220 {
4221     if (!d->m_synctex_scanner)
4222         return nullptr;
4223 
4224     const QSizeF dpi = d->m_generator->dpi();
4225 
4226     if (synctex_edit_query(d->m_synctex_scanner, pageNr + 1, absX * 72. / dpi.width(), absY * 72. / dpi.height()) > 0) {
4227         synctex_node_p node;
4228         // TODO what should we do if there is really more than one node?
4229         while ((node = synctex_scanner_next_result(d->m_synctex_scanner))) {
4230             int line = synctex_node_line(node);
4231             int col = synctex_node_column(node);
4232             // column extraction does not seem to be implemented in synctex so far. set the SourceReference default value.
4233             if (col == -1) {
4234                 col = 0;
4235             }
4236             const char *name = synctex_scanner_get_name(d->m_synctex_scanner, synctex_node_tag(node));
4237 
4238             return new Okular::SourceReference(QFile::decodeName(name), line, col);
4239         }
4240     }
4241     return nullptr;
4242 }
4243 
printingSupport() const4244 Document::PrintingType Document::printingSupport() const
4245 {
4246     if (d->m_generator) {
4247         if (d->m_generator->hasFeature(Generator::PrintNative)) {
4248             return NativePrinting;
4249         }
4250 
4251 #ifndef Q_OS_WIN
4252         if (d->m_generator->hasFeature(Generator::PrintPostscript)) {
4253             return PostscriptPrinting;
4254         }
4255 #endif
4256     }
4257 
4258     return NoPrinting;
4259 }
4260 
supportsPrintToFile() const4261 bool Document::supportsPrintToFile() const
4262 {
4263     return d->m_generator ? d->m_generator->hasFeature(Generator::PrintToFile) : false;
4264 }
4265 
print(QPrinter & printer)4266 bool Document::print(QPrinter &printer)
4267 {
4268     return d->m_generator ? d->m_generator->print(printer) : false;
4269 }
4270 
printError() const4271 QString Document::printError() const
4272 {
4273     Okular::Generator::PrintError err = Generator::UnknownPrintError;
4274     if (d->m_generator) {
4275         QMetaObject::invokeMethod(d->m_generator, "printError", Qt::DirectConnection, Q_RETURN_ARG(Okular::Generator::PrintError, err));
4276     }
4277     Q_ASSERT(err != Generator::NoPrintError);
4278     switch (err) {
4279     case Generator::TemporaryFileOpenPrintError:
4280         return i18n("Could not open a temporary file");
4281     case Generator::FileConversionPrintError:
4282         return i18n("Print conversion failed");
4283     case Generator::PrintingProcessCrashPrintError:
4284         return i18n("Printing process crashed");
4285     case Generator::PrintingProcessStartPrintError:
4286         return i18n("Printing process could not start");
4287     case Generator::PrintToFilePrintError:
4288         return i18n("Printing to file failed");
4289     case Generator::InvalidPrinterStatePrintError:
4290         return i18n("Printer was in invalid state");
4291     case Generator::UnableToFindFilePrintError:
4292         return i18n("Unable to find file to print");
4293     case Generator::NoFileToPrintError:
4294         return i18n("There was no file to print");
4295     case Generator::NoBinaryToPrintError:
4296         return i18n("Could not find a suitable binary for printing. Make sure CUPS lpr binary is available");
4297     case Generator::InvalidPageSizePrintError:
4298         return i18n("The page print size is invalid");
4299     case Generator::NoPrintError:
4300         return QString();
4301     case Generator::UnknownPrintError:
4302         return QString();
4303     }
4304 
4305     return QString();
4306 }
4307 
printConfigurationWidget() const4308 QWidget *Document::printConfigurationWidget() const
4309 {
4310     if (d->m_generator) {
4311         PrintInterface *iface = qobject_cast<Okular::PrintInterface *>(d->m_generator);
4312         return iface ? iface->printConfigurationWidget() : nullptr;
4313     } else
4314         return nullptr;
4315 }
4316 
fillConfigDialog(KConfigDialog * dialog)4317 void Document::fillConfigDialog(KConfigDialog *dialog)
4318 {
4319     if (!dialog)
4320         return;
4321 
4322     // We know it's a BackendConfigDialog, but check anyway
4323     BackendConfigDialog *bcd = dynamic_cast<BackendConfigDialog *>(dialog);
4324     if (!bcd)
4325         return;
4326 
4327     // ensure that we have all the generators with settings loaded
4328     QVector<KPluginMetaData> offers = DocumentPrivate::configurableGenerators();
4329     d->loadServiceList(offers);
4330 
4331     // We want the generators to be sorted by name so let's fill in a QMap
4332     // this sorts by internal id which is not awesome, but at least the sorting
4333     // is stable between runs that before it wasn't
4334     QMap<QString, GeneratorInfo> sortedGenerators;
4335     QHash<QString, GeneratorInfo>::iterator it = d->m_loadedGenerators.begin();
4336     QHash<QString, GeneratorInfo>::iterator itEnd = d->m_loadedGenerators.end();
4337     for (; it != itEnd; ++it) {
4338         sortedGenerators.insert(it.key(), it.value());
4339     }
4340 
4341     bool pagesAdded = false;
4342     QMap<QString, GeneratorInfo>::iterator sit = sortedGenerators.begin();
4343     QMap<QString, GeneratorInfo>::iterator sitEnd = sortedGenerators.end();
4344     for (; sit != sitEnd; ++sit) {
4345         Okular::ConfigInterface *iface = d->generatorConfig(sit.value());
4346         if (iface) {
4347             iface->addPages(dialog);
4348             pagesAdded = true;
4349 
4350             if (sit.value().generator == d->m_generator) {
4351                 const int rowCount = bcd->thePageWidget()->model()->rowCount();
4352                 KPageView *view = bcd->thePageWidget();
4353                 view->setCurrentPage(view->model()->index(rowCount - 1, 0));
4354             }
4355         }
4356     }
4357     if (pagesAdded) {
4358         connect(dialog, &KConfigDialog::settingsChanged, this, [this] { d->slotGeneratorConfigChanged(); });
4359     }
4360 }
4361 
configurableGenerators()4362 QVector<KPluginMetaData> DocumentPrivate::configurableGenerators()
4363 {
4364     const QVector<KPluginMetaData> available = availableGenerators();
4365     QVector<KPluginMetaData> result;
4366     for (const KPluginMetaData &md : available) {
4367         if (md.rawData()[QStringLiteral("X-KDE-okularHasInternalSettings")].toBool()) {
4368             result << md;
4369         }
4370     }
4371     return result;
4372 }
4373 
generatorInfo() const4374 KPluginMetaData Document::generatorInfo() const
4375 {
4376     if (!d->m_generator)
4377         return KPluginMetaData();
4378 
4379     auto genIt = d->m_loadedGenerators.constFind(d->m_generatorName);
4380     Q_ASSERT(genIt != d->m_loadedGenerators.constEnd());
4381     return genIt.value().metadata;
4382 }
4383 
configurableGenerators() const4384 int Document::configurableGenerators() const
4385 {
4386     return DocumentPrivate::configurableGenerators().size();
4387 }
4388 
supportedMimeTypes() const4389 QStringList Document::supportedMimeTypes() const
4390 {
4391     // TODO: make it a static member of DocumentPrivate?
4392     QStringList result = d->m_supportedMimeTypes;
4393     if (result.isEmpty()) {
4394         const QVector<KPluginMetaData> available = DocumentPrivate::availableGenerators();
4395         for (const KPluginMetaData &md : available) {
4396             result << md.mimeTypes();
4397         }
4398 
4399         // Remove duplicate mimetypes represented by different names
4400         QMimeDatabase mimeDatabase;
4401         QSet<QMimeType> uniqueMimetypes;
4402         for (const QString &mimeName : qAsConst(result)) {
4403             uniqueMimetypes.insert(mimeDatabase.mimeTypeForName(mimeName));
4404         }
4405         result.clear();
4406         for (const QMimeType &mimeType : uniqueMimetypes) {
4407             result.append(mimeType.name());
4408         }
4409 
4410         // Add the Okular archive mimetype
4411         result << QStringLiteral("application/vnd.kde.okular-archive");
4412 
4413         // Sorting by mimetype name doesn't make a ton of sense,
4414         // but ensures that the list is ordered the same way every time
4415         std::sort(result.begin(), result.end());
4416 
4417         d->m_supportedMimeTypes = result;
4418     }
4419     return result;
4420 }
4421 
canSwapBackingFile() const4422 bool Document::canSwapBackingFile() const
4423 {
4424     if (!d->m_generator)
4425         return false;
4426 
4427     return d->m_generator->hasFeature(Generator::SwapBackingFile);
4428 }
4429 
swapBackingFile(const QString & newFileName,const QUrl & url)4430 bool Document::swapBackingFile(const QString &newFileName, const QUrl &url)
4431 {
4432     if (!d->m_generator)
4433         return false;
4434 
4435     if (!d->m_generator->hasFeature(Generator::SwapBackingFile))
4436         return false;
4437 
4438     // Save metadata about the file we're about to close
4439     d->saveDocumentInfo();
4440 
4441     d->clearAndWaitForRequests();
4442 
4443     qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName;
4444     QVector<Page *> newPagesVector;
4445     Generator::SwapBackingFileResult result = d->m_generator->swapBackingFile(newFileName, newPagesVector);
4446     if (result != Generator::SwapBackingFileError) {
4447         QLinkedList<ObjectRect *> rectsToDelete;
4448         QLinkedList<Annotation *> annotationsToDelete;
4449         QSet<PagePrivate *> pagePrivatesToDelete;
4450 
4451         if (result == Generator::SwapBackingFileReloadInternalData) {
4452             // Here we need to replace everything that the old generator
4453             // had created with what the new one has without making it look like
4454             // we have actually closed and opened the file again
4455 
4456             // Simple sanity check
4457             if (newPagesVector.count() != d->m_pagesVector.count())
4458                 return false;
4459 
4460             // Update the undo stack contents
4461             for (int i = 0; i < d->m_undoStack->count(); ++i) {
4462                 // Trust me on the const_cast ^_^
4463                 QUndoCommand *uc = const_cast<QUndoCommand *>(d->m_undoStack->command(i));
4464                 if (OkularUndoCommand *ouc = dynamic_cast<OkularUndoCommand *>(uc)) {
4465                     const bool success = ouc->refreshInternalPageReferences(newPagesVector);
4466                     if (!success) {
4467                         qWarning() << "Document::swapBackingFile: refreshInternalPageReferences failed" << ouc;
4468                         return false;
4469                     }
4470                 } else {
4471                     qWarning() << "Document::swapBackingFile: Unhandled undo command" << uc;
4472                     return false;
4473                 }
4474             }
4475 
4476             for (int i = 0; i < d->m_pagesVector.count(); ++i) {
4477                 // switch the PagePrivate* from newPage to oldPage
4478                 // this way everyone still holding Page* doesn't get
4479                 // disturbed by it
4480                 Page *oldPage = d->m_pagesVector[i];
4481                 Page *newPage = newPagesVector[i];
4482                 newPage->d->adoptGeneratedContents(oldPage->d);
4483 
4484                 pagePrivatesToDelete << oldPage->d;
4485                 oldPage->d = newPage->d;
4486                 oldPage->d->m_page = oldPage;
4487                 oldPage->d->m_doc = d;
4488                 newPage->d = nullptr;
4489 
4490                 annotationsToDelete << oldPage->m_annotations;
4491                 rectsToDelete << oldPage->m_rects;
4492                 oldPage->m_annotations = newPage->m_annotations;
4493                 oldPage->m_rects = newPage->m_rects;
4494             }
4495             qDeleteAll(newPagesVector);
4496         }
4497 
4498         d->m_url = url;
4499         d->m_docFileName = newFileName;
4500         d->updateMetadataXmlNameAndDocSize();
4501         d->m_bookmarkManager->setUrl(d->m_url);
4502         d->m_documentInfo = DocumentInfo();
4503         d->m_documentInfoAskedKeys.clear();
4504 
4505         if (d->m_synctex_scanner) {
4506             synctex_scanner_free(d->m_synctex_scanner);
4507             d->m_synctex_scanner = synctex_scanner_new_with_output_file(QFile::encodeName(newFileName).constData(), nullptr, 1);
4508             if (!d->m_synctex_scanner && QFile::exists(newFileName + QLatin1String("sync"))) {
4509                 d->loadSyncFile(newFileName);
4510             }
4511         }
4512 
4513         foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::UrlChanged));
4514 
4515         qDeleteAll(annotationsToDelete);
4516         qDeleteAll(rectsToDelete);
4517         qDeleteAll(pagePrivatesToDelete);
4518 
4519         return true;
4520     } else {
4521         return false;
4522     }
4523 }
4524 
swapBackingFileArchive(const QString & newFileName,const QUrl & url)4525 bool Document::swapBackingFileArchive(const QString &newFileName, const QUrl &url)
4526 {
4527     qCDebug(OkularCoreDebug) << "Swapping backing archive to" << newFileName;
4528 
4529     ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive(newFileName);
4530     if (!newArchive)
4531         return false;
4532 
4533     const QString tempFileName = newArchive->document.fileName();
4534 
4535     const bool success = swapBackingFile(tempFileName, url);
4536 
4537     if (success) {
4538         delete d->m_archiveData;
4539         d->m_archiveData = newArchive;
4540     }
4541 
4542     return success;
4543 }
4544 
setHistoryClean(bool clean)4545 void Document::setHistoryClean(bool clean)
4546 {
4547     if (clean)
4548         d->m_undoStack->setClean();
4549     else
4550         d->m_undoStack->resetClean();
4551 }
4552 
isHistoryClean() const4553 bool Document::isHistoryClean() const
4554 {
4555     return d->m_undoStack->isClean();
4556 }
4557 
canSaveChanges() const4558 bool Document::canSaveChanges() const
4559 {
4560     if (!d->m_generator)
4561         return false;
4562     Q_ASSERT(!d->m_generatorName.isEmpty());
4563 
4564     QHash<QString, GeneratorInfo>::iterator genIt = d->m_loadedGenerators.find(d->m_generatorName);
4565     Q_ASSERT(genIt != d->m_loadedGenerators.end());
4566     SaveInterface *saveIface = d->generatorSave(genIt.value());
4567     if (!saveIface)
4568         return false;
4569 
4570     return saveIface->supportsOption(SaveInterface::SaveChanges);
4571 }
4572 
canSaveChanges(SaveCapability cap) const4573 bool Document::canSaveChanges(SaveCapability cap) const
4574 {
4575     switch (cap) {
4576     case SaveFormsCapability:
4577         /* Assume that if the generator supports saving, forms can be saved.
4578          * We have no means to actually query the generator at the moment
4579          * TODO: Add some method to query the generator in SaveInterface */
4580         return canSaveChanges();
4581 
4582     case SaveAnnotationsCapability:
4583         return d->canAddAnnotationsNatively();
4584     }
4585 
4586     return false;
4587 }
4588 
saveChanges(const QString & fileName)4589 bool Document::saveChanges(const QString &fileName)
4590 {
4591     QString errorText;
4592     return saveChanges(fileName, &errorText);
4593 }
4594 
saveChanges(const QString & fileName,QString * errorText)4595 bool Document::saveChanges(const QString &fileName, QString *errorText)
4596 {
4597     if (!d->m_generator || fileName.isEmpty())
4598         return false;
4599     Q_ASSERT(!d->m_generatorName.isEmpty());
4600 
4601     QHash<QString, GeneratorInfo>::iterator genIt = d->m_loadedGenerators.find(d->m_generatorName);
4602     Q_ASSERT(genIt != d->m_loadedGenerators.end());
4603     SaveInterface *saveIface = d->generatorSave(genIt.value());
4604     if (!saveIface || !saveIface->supportsOption(SaveInterface::SaveChanges))
4605         return false;
4606 
4607     return saveIface->save(fileName, SaveInterface::SaveChanges, errorText);
4608 }
4609 
registerView(View * view)4610 void Document::registerView(View *view)
4611 {
4612     if (!view)
4613         return;
4614 
4615     Document *viewDoc = view->viewDocument();
4616     if (viewDoc) {
4617         // check if already registered for this document
4618         if (viewDoc == this)
4619             return;
4620 
4621         viewDoc->unregisterView(view);
4622     }
4623 
4624     d->m_views.insert(view);
4625     view->d_func()->document = d;
4626 }
4627 
unregisterView(View * view)4628 void Document::unregisterView(View *view)
4629 {
4630     if (!view)
4631         return;
4632 
4633     Document *viewDoc = view->viewDocument();
4634     if (!viewDoc || viewDoc != this)
4635         return;
4636 
4637     view->d_func()->document = nullptr;
4638     d->m_views.remove(view);
4639 }
4640 
fontData(const FontInfo & font) const4641 QByteArray Document::fontData(const FontInfo &font) const
4642 {
4643     QByteArray result;
4644 
4645     if (d->m_generator) {
4646         // clang-format off
4647         // Otherwise the Q_ARG(QByteArray* gets broken
4648         QMetaObject::invokeMethod(d->m_generator, "requestFontData", Qt::DirectConnection, Q_ARG(Okular::FontInfo, font), Q_ARG(QByteArray*, &result));
4649         // clang-format on
4650     }
4651 
4652     return result;
4653 }
4654 
unpackDocumentArchive(const QString & archivePath)4655 ArchiveData *DocumentPrivate::unpackDocumentArchive(const QString &archivePath)
4656 {
4657     QMimeDatabase db;
4658     const QMimeType mime = db.mimeTypeForFile(archivePath, QMimeDatabase::MatchExtension);
4659     if (!mime.inherits(QStringLiteral("application/vnd.kde.okular-archive")))
4660         return nullptr;
4661 
4662     KZip okularArchive(archivePath);
4663     if (!okularArchive.open(QIODevice::ReadOnly))
4664         return nullptr;
4665 
4666     const KArchiveDirectory *mainDir = okularArchive.directory();
4667 
4668     // Check the archive doesn't have folders, we don't create them when saving the archive
4669     // and folders mean paths and paths mean path traversal issues
4670     const QStringList mainDirEntries = mainDir->entries();
4671     for (const QString &entry : mainDirEntries) {
4672         if (mainDir->entry(entry)->isDirectory()) {
4673             qWarning() << "Warning: Found a directory inside" << archivePath << " - Okular does not create files like that so it is most probably forged.";
4674             return nullptr;
4675         }
4676     }
4677 
4678     const KArchiveEntry *mainEntry = mainDir->entry(QStringLiteral("content.xml"));
4679     if (!mainEntry || !mainEntry->isFile())
4680         return nullptr;
4681 
4682     std::unique_ptr<QIODevice> mainEntryDevice(static_cast<const KZipFileEntry *>(mainEntry)->createDevice());
4683     QDomDocument doc;
4684     if (!doc.setContent(mainEntryDevice.get()))
4685         return nullptr;
4686     mainEntryDevice.reset();
4687 
4688     QDomElement root = doc.documentElement();
4689     if (root.tagName() != QLatin1String("OkularArchive"))
4690         return nullptr;
4691 
4692     QString documentFileName;
4693     QString metadataFileName;
4694     QDomElement el = root.firstChild().toElement();
4695     for (; !el.isNull(); el = el.nextSibling().toElement()) {
4696         if (el.tagName() == QLatin1String("Files")) {
4697             QDomElement fileEl = el.firstChild().toElement();
4698             for (; !fileEl.isNull(); fileEl = fileEl.nextSibling().toElement()) {
4699                 if (fileEl.tagName() == QLatin1String("DocumentFileName"))
4700                     documentFileName = fileEl.text();
4701                 else if (fileEl.tagName() == QLatin1String("MetadataFileName"))
4702                     metadataFileName = fileEl.text();
4703             }
4704         }
4705     }
4706     if (documentFileName.isEmpty())
4707         return nullptr;
4708 
4709     const KArchiveEntry *docEntry = mainDir->entry(documentFileName);
4710     if (!docEntry || !docEntry->isFile())
4711         return nullptr;
4712 
4713     std::unique_ptr<ArchiveData> archiveData(new ArchiveData());
4714     const int dotPos = documentFileName.indexOf(QLatin1Char('.'));
4715     if (dotPos != -1)
4716         archiveData->document.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX") + documentFileName.mid(dotPos));
4717     if (!archiveData->document.open())
4718         return nullptr;
4719 
4720     archiveData->originalFileName = documentFileName;
4721 
4722     {
4723         std::unique_ptr<QIODevice> docEntryDevice(static_cast<const KZipFileEntry *>(docEntry)->createDevice());
4724         copyQIODevice(docEntryDevice.get(), &archiveData->document);
4725         archiveData->document.close();
4726     }
4727 
4728     const KArchiveEntry *metadataEntry = mainDir->entry(metadataFileName);
4729     if (metadataEntry && metadataEntry->isFile()) {
4730         std::unique_ptr<QIODevice> metadataEntryDevice(static_cast<const KZipFileEntry *>(metadataEntry)->createDevice());
4731         archiveData->metadataFile.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX.xml"));
4732         if (archiveData->metadataFile.open()) {
4733             copyQIODevice(metadataEntryDevice.get(), &archiveData->metadataFile);
4734             archiveData->metadataFile.close();
4735         }
4736     }
4737 
4738     return archiveData.release();
4739 }
4740 
openDocumentArchive(const QString & docFile,const QUrl & url,const QString & password)4741 Document::OpenResult Document::openDocumentArchive(const QString &docFile, const QUrl &url, const QString &password)
4742 {
4743     d->m_archiveData = DocumentPrivate::unpackDocumentArchive(docFile);
4744     if (!d->m_archiveData)
4745         return OpenError;
4746 
4747     const QString tempFileName = d->m_archiveData->document.fileName();
4748     QMimeDatabase db;
4749     const QMimeType docMime = db.mimeTypeForFile(tempFileName, QMimeDatabase::MatchExtension);
4750     const OpenResult ret = openDocument(tempFileName, url, docMime, password);
4751 
4752     if (ret != OpenSuccess) {
4753         delete d->m_archiveData;
4754         d->m_archiveData = nullptr;
4755     }
4756 
4757     return ret;
4758 }
4759 
saveDocumentArchive(const QString & fileName)4760 bool Document::saveDocumentArchive(const QString &fileName)
4761 {
4762     if (!d->m_generator)
4763         return false;
4764 
4765     /* If we opened an archive, use the name of original file (eg foo.pdf)
4766      * instead of the archive's one (eg foo.okular) */
4767     QString docFileName = d->m_archiveData ? d->m_archiveData->originalFileName : d->m_url.fileName();
4768     if (docFileName == QLatin1String("-"))
4769         return false;
4770 
4771     QString docPath = d->m_docFileName;
4772     const QFileInfo fi(docPath);
4773     if (fi.isSymLink())
4774         docPath = fi.symLinkTarget();
4775 
4776     KZip okularArchive(fileName);
4777     if (!okularArchive.open(QIODevice::WriteOnly))
4778         return false;
4779 
4780     const KUser user;
4781 #ifndef Q_OS_WIN
4782     const KUserGroup userGroup(user.groupId());
4783 #else
4784     const KUserGroup userGroup(QString(""));
4785 #endif
4786 
4787     QDomDocument contentDoc(QStringLiteral("OkularArchive"));
4788     QDomProcessingInstruction xmlPi = contentDoc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
4789     contentDoc.appendChild(xmlPi);
4790     QDomElement root = contentDoc.createElement(QStringLiteral("OkularArchive"));
4791     contentDoc.appendChild(root);
4792 
4793     QDomElement filesNode = contentDoc.createElement(QStringLiteral("Files"));
4794     root.appendChild(filesNode);
4795 
4796     QDomElement fileNameNode = contentDoc.createElement(QStringLiteral("DocumentFileName"));
4797     filesNode.appendChild(fileNameNode);
4798     fileNameNode.appendChild(contentDoc.createTextNode(docFileName));
4799 
4800     QDomElement metadataFileNameNode = contentDoc.createElement(QStringLiteral("MetadataFileName"));
4801     filesNode.appendChild(metadataFileNameNode);
4802     metadataFileNameNode.appendChild(contentDoc.createTextNode(QStringLiteral("metadata.xml")));
4803 
4804     // If the generator can save annotations natively, do it
4805     QTemporaryFile modifiedFile;
4806     bool annotationsSavedNatively = false;
4807     bool formsSavedNatively = false;
4808     if (d->canAddAnnotationsNatively() || canSaveChanges(SaveFormsCapability)) {
4809         if (!modifiedFile.open())
4810             return false;
4811 
4812         const QString modifiedFileName = modifiedFile.fileName();
4813 
4814         modifiedFile.close(); // We're only interested in the file name
4815 
4816         QString errorText;
4817         if (saveChanges(modifiedFileName, &errorText)) {
4818             docPath = modifiedFileName; // Save this instead of the original file
4819             annotationsSavedNatively = d->canAddAnnotationsNatively();
4820             formsSavedNatively = canSaveChanges(SaveFormsCapability);
4821         } else {
4822             qCWarning(OkularCoreDebug) << "saveChanges failed: " << errorText;
4823             qCDebug(OkularCoreDebug) << "Falling back to saving a copy of the original file";
4824         }
4825     }
4826 
4827     PageItems saveWhat = None;
4828     if (!annotationsSavedNatively)
4829         saveWhat |= AnnotationPageItems;
4830     if (!formsSavedNatively)
4831         saveWhat |= FormFieldPageItems;
4832 
4833     QTemporaryFile metadataFile;
4834     if (!d->savePageDocumentInfo(&metadataFile, saveWhat))
4835         return false;
4836 
4837     const QByteArray contentDocXml = contentDoc.toByteArray();
4838     const mode_t perm = 0100644;
4839     okularArchive.writeFile(QStringLiteral("content.xml"), contentDocXml, perm, user.loginName(), userGroup.name());
4840 
4841     okularArchive.addLocalFile(docPath, docFileName);
4842     okularArchive.addLocalFile(metadataFile.fileName(), QStringLiteral("metadata.xml"));
4843 
4844     if (!okularArchive.close())
4845         return false;
4846 
4847     return true;
4848 }
4849 
extractArchivedFile(const QString & destFileName)4850 bool Document::extractArchivedFile(const QString &destFileName)
4851 {
4852     if (!d->m_archiveData)
4853         return false;
4854 
4855     // Remove existing file, if present (QFile::copy doesn't overwrite by itself)
4856     QFile::remove(destFileName);
4857 
4858     return d->m_archiveData->document.copy(destFileName);
4859 }
4860 
orientation() const4861 QPrinter::Orientation Document::orientation() const
4862 {
4863     double width, height;
4864     int landscape, portrait;
4865     const Okular::Page *currentPage;
4866 
4867     // if some pages are landscape and others are not, the most common wins, as
4868     // QPrinter does not accept a per-page setting
4869     landscape = 0;
4870     portrait = 0;
4871     for (uint i = 0; i < pages(); i++) {
4872         currentPage = page(i);
4873         width = currentPage->width();
4874         height = currentPage->height();
4875         if (currentPage->orientation() == Okular::Rotation90 || currentPage->orientation() == Okular::Rotation270)
4876             qSwap(width, height);
4877         if (width > height)
4878             landscape++;
4879         else
4880             portrait++;
4881     }
4882     return (landscape > portrait) ? QPrinter::Landscape : QPrinter::Portrait;
4883 }
4884 
setAnnotationEditingEnabled(bool enable)4885 void Document::setAnnotationEditingEnabled(bool enable)
4886 {
4887     d->m_annotationEditingEnabled = enable;
4888     foreachObserver(notifySetup(d->m_pagesVector, 0));
4889 }
4890 
walletDataForFile(const QString & fileName,QString * walletName,QString * walletFolder,QString * walletKey) const4891 void Document::walletDataForFile(const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey) const
4892 {
4893     if (d->m_generator) {
4894         d->m_generator->walletDataForFile(fileName, walletName, walletFolder, walletKey);
4895     } else if (d->m_walletGenerator) {
4896         d->m_walletGenerator->walletDataForFile(fileName, walletName, walletFolder, walletKey);
4897     }
4898 }
4899 
isDocdataMigrationNeeded() const4900 bool Document::isDocdataMigrationNeeded() const
4901 {
4902     return d->m_docdataMigrationNeeded;
4903 }
4904 
docdataMigrationDone()4905 void Document::docdataMigrationDone()
4906 {
4907     if (d->m_docdataMigrationNeeded) {
4908         d->m_docdataMigrationNeeded = false;
4909         foreachObserver(notifySetup(d->m_pagesVector, 0));
4910     }
4911 }
4912 
layersModel() const4913 QAbstractItemModel *Document::layersModel() const
4914 {
4915     return d->m_generator ? d->m_generator->layersModel() : nullptr;
4916 }
4917 
openError() const4918 QString Document::openError() const
4919 {
4920     return d->m_openError;
4921 }
4922 
requestSignedRevisionData(const Okular::SignatureInfo & info)4923 QByteArray Document::requestSignedRevisionData(const Okular::SignatureInfo &info)
4924 {
4925     QFile f(d->m_docFileName);
4926     if (!f.open(QIODevice::ReadOnly)) {
4927         emit error(i18n("Could not open '%1'. File does not exist", d->m_docFileName), -1);
4928         return {};
4929     }
4930 
4931     const QList<qint64> byteRange = info.signedRangeBounds();
4932     f.seek(byteRange.first());
4933     QByteArray data = f.read(byteRange.last() - byteRange.first());
4934     f.close();
4935 
4936     return data;
4937 }
4938 
refreshPixmaps(int pageNumber)4939 void Document::refreshPixmaps(int pageNumber)
4940 {
4941     d->refreshPixmaps(pageNumber);
4942 }
4943 
executeScript(const QString & function)4944 void DocumentPrivate::executeScript(const QString &function)
4945 {
4946     if (!m_scripter)
4947         m_scripter = new Scripter(this);
4948     m_scripter->execute(JavaScript, function);
4949 }
4950 
requestDone(PixmapRequest * req)4951 void DocumentPrivate::requestDone(PixmapRequest *req)
4952 {
4953     if (!req)
4954         return;
4955 
4956     if (!m_generator || m_closingLoop) {
4957         m_pixmapRequestsMutex.lock();
4958         m_executingPixmapRequests.removeAll(req);
4959         m_pixmapRequestsMutex.unlock();
4960         delete req;
4961         if (m_closingLoop)
4962             m_closingLoop->exit();
4963         return;
4964     }
4965 
4966 #ifndef NDEBUG
4967     if (!m_generator->canGeneratePixmap())
4968         qCDebug(OkularCoreDebug) << "requestDone with generator not in READY state.";
4969 #endif
4970 
4971     if (!req->shouldAbortRender()) {
4972         // [MEM] 1.1 find and remove a previous entry for the same page and id
4973         QLinkedList<AllocatedPixmap *>::iterator aIt = m_allocatedPixmaps.begin();
4974         QLinkedList<AllocatedPixmap *>::iterator aEnd = m_allocatedPixmaps.end();
4975         for (; aIt != aEnd; ++aIt)
4976             if ((*aIt)->page == req->pageNumber() && (*aIt)->observer == req->observer()) {
4977                 AllocatedPixmap *p = *aIt;
4978                 m_allocatedPixmaps.erase(aIt);
4979                 m_allocatedPixmapsTotalMemory -= p->memory;
4980                 delete p;
4981                 break;
4982             }
4983 
4984         DocumentObserver *observer = req->observer();
4985         if (m_observers.contains(observer)) {
4986             // [MEM] 1.2 append memory allocation descriptor to the FIFO
4987             qulonglong memoryBytes = 0;
4988             const TilesManager *tm = req->d->tilesManager();
4989             if (tm)
4990                 memoryBytes = tm->totalMemory();
4991             else
4992                 memoryBytes = 4 * req->width() * req->height();
4993 
4994             AllocatedPixmap *memoryPage = new AllocatedPixmap(req->observer(), req->pageNumber(), memoryBytes);
4995             m_allocatedPixmaps.append(memoryPage);
4996             m_allocatedPixmapsTotalMemory += memoryBytes;
4997 
4998             // 2. notify an observer that its pixmap changed
4999             observer->notifyPageChanged(req->pageNumber(), DocumentObserver::Pixmap);
5000         }
5001 #ifndef NDEBUG
5002         else
5003             qCWarning(OkularCoreDebug) << "Receiving a done request for the defunct observer" << observer;
5004 #endif
5005     }
5006 
5007     // 3. delete request
5008     m_pixmapRequestsMutex.lock();
5009     m_executingPixmapRequests.removeAll(req);
5010     m_pixmapRequestsMutex.unlock();
5011     delete req;
5012 
5013     // 4. start a new generation if some is pending
5014     m_pixmapRequestsMutex.lock();
5015     bool hasPixmaps = !m_pixmapRequestsStack.isEmpty();
5016     m_pixmapRequestsMutex.unlock();
5017     if (hasPixmaps)
5018         sendGeneratorPixmapRequest();
5019 }
5020 
setPageBoundingBox(int page,const NormalizedRect & boundingBox)5021 void DocumentPrivate::setPageBoundingBox(int page, const NormalizedRect &boundingBox)
5022 {
5023     Page *kp = m_pagesVector[page];
5024     if (!m_generator || !kp)
5025         return;
5026 
5027     if (kp->boundingBox() == boundingBox)
5028         return;
5029     kp->setBoundingBox(boundingBox);
5030 
5031     // notify observers about the change
5032     foreachObserverD(notifyPageChanged(page, DocumentObserver::BoundingBox));
5033 
5034     // 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.
5035     // TODO: Crop computation should also consider annotations, actions, etc. to make sure they're not cropped away.
5036     // TODO: Help compute bounding box for generators that create a QPixmap without a QImage, like text and plucker.
5037     // TODO: Don't compute the bounding box if no one needs it (e.g., Trim Borders is off).
5038 }
5039 
calculateMaxTextPages()5040 void DocumentPrivate::calculateMaxTextPages()
5041 {
5042     int multipliers = qMax(1, qRound(getTotalMemory() / 536870912.0)); // 512 MB
5043     switch (SettingsCore::memoryLevel()) {
5044     case SettingsCore::EnumMemoryLevel::Low:
5045         m_maxAllocatedTextPages = multipliers * 2;
5046         break;
5047 
5048     case SettingsCore::EnumMemoryLevel::Normal:
5049         m_maxAllocatedTextPages = multipliers * 50;
5050         break;
5051 
5052     case SettingsCore::EnumMemoryLevel::Aggressive:
5053         m_maxAllocatedTextPages = multipliers * 250;
5054         break;
5055 
5056     case SettingsCore::EnumMemoryLevel::Greedy:
5057         m_maxAllocatedTextPages = multipliers * 1250;
5058         break;
5059     }
5060 }
5061 
textGenerationDone(Page * page)5062 void DocumentPrivate::textGenerationDone(Page *page)
5063 {
5064     if (!m_pageController)
5065         return;
5066 
5067     // 1. If we reached the cache limit, delete the first text page from the fifo
5068     if (m_allocatedTextPagesFifo.size() == m_maxAllocatedTextPages) {
5069         int pageToKick = m_allocatedTextPagesFifo.takeFirst();
5070         if (pageToKick != page->number()) // this should never happen but better be safe than sorry
5071         {
5072             m_pagesVector.at(pageToKick)->setTextPage(nullptr); // deletes the textpage
5073         }
5074     }
5075 
5076     // 2. Add the page to the fifo of generated text pages
5077     m_allocatedTextPagesFifo.append(page->number());
5078 }
5079 
setRotation(int r)5080 void Document::setRotation(int r)
5081 {
5082     d->setRotationInternal(r, true);
5083 }
5084 
setRotationInternal(int r,bool notify)5085 void DocumentPrivate::setRotationInternal(int r, bool notify)
5086 {
5087     Rotation rotation = (Rotation)r;
5088     if (!m_generator || (m_rotation == rotation))
5089         return;
5090 
5091     // tell the pages to rotate
5092     QVector<Okular::Page *>::const_iterator pIt = m_pagesVector.constBegin();
5093     QVector<Okular::Page *>::const_iterator pEnd = m_pagesVector.constEnd();
5094     for (; pIt != pEnd; ++pIt)
5095         (*pIt)->d->rotateAt(rotation);
5096     if (notify) {
5097         // notify the generator that the current rotation has changed
5098         m_generator->rotationChanged(rotation, m_rotation);
5099     }
5100     // set the new rotation
5101     m_rotation = rotation;
5102 
5103     if (notify) {
5104         foreachObserverD(notifySetup(m_pagesVector, DocumentObserver::NewLayoutForPages));
5105         foreachObserverD(notifyContentsCleared(DocumentObserver::Pixmap | DocumentObserver::Highlights | DocumentObserver::Annotations));
5106     }
5107     qCDebug(OkularCoreDebug) << "Rotated:" << r;
5108 }
5109 
setPageSize(const PageSize & size)5110 void Document::setPageSize(const PageSize &size)
5111 {
5112     if (!d->m_generator || !d->m_generator->hasFeature(Generator::PageSizes))
5113         return;
5114 
5115     if (d->m_pageSizes.isEmpty())
5116         d->m_pageSizes = d->m_generator->pageSizes();
5117     int sizeid = d->m_pageSizes.indexOf(size);
5118     if (sizeid == -1)
5119         return;
5120 
5121     // tell the pages to change size
5122     QVector<Okular::Page *>::const_iterator pIt = d->m_pagesVector.constBegin();
5123     QVector<Okular::Page *>::const_iterator pEnd = d->m_pagesVector.constEnd();
5124     for (; pIt != pEnd; ++pIt)
5125         (*pIt)->d->changeSize(size);
5126     // clear 'memory allocation' descriptors
5127     qDeleteAll(d->m_allocatedPixmaps);
5128     d->m_allocatedPixmaps.clear();
5129     d->m_allocatedPixmapsTotalMemory = 0;
5130     // notify the generator that the current page size has changed
5131     d->m_generator->pageSizeChanged(size, d->m_pageSize);
5132     // set the new page size
5133     d->m_pageSize = size;
5134 
5135     foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::NewLayoutForPages));
5136     foreachObserver(notifyContentsCleared(DocumentObserver::Pixmap | DocumentObserver::Highlights));
5137     qCDebug(OkularCoreDebug) << "New PageSize id:" << sizeid;
5138 }
5139 
5140 /** DocumentViewport **/
5141 
DocumentViewport(int n)5142 DocumentViewport::DocumentViewport(int n)
5143     : pageNumber(n)
5144 {
5145     // default settings
5146     rePos.enabled = false;
5147     rePos.normalizedX = 0.5;
5148     rePos.normalizedY = 0.0;
5149     rePos.pos = Center;
5150     autoFit.enabled = false;
5151     autoFit.width = false;
5152     autoFit.height = false;
5153 }
5154 
DocumentViewport(const QString & xmlDesc)5155 DocumentViewport::DocumentViewport(const QString &xmlDesc)
5156     : pageNumber(-1)
5157 {
5158     // default settings (maybe overridden below)
5159     rePos.enabled = false;
5160     rePos.normalizedX = 0.5;
5161     rePos.normalizedY = 0.0;
5162     rePos.pos = Center;
5163     autoFit.enabled = false;
5164     autoFit.width = false;
5165     autoFit.height = false;
5166 
5167     // check for string presence
5168     if (xmlDesc.isEmpty())
5169         return;
5170 
5171     // decode the string
5172     bool ok;
5173     int field = 0;
5174     QString token = xmlDesc.section(QLatin1Char(';'), field, field);
5175     while (!token.isEmpty()) {
5176         // decode the current token
5177         if (field == 0) {
5178             pageNumber = token.toInt(&ok);
5179             if (!ok)
5180                 return;
5181         } else if (token.startsWith(QLatin1String("C1"))) {
5182             rePos.enabled = true;
5183             rePos.normalizedX = token.section(QLatin1Char(':'), 1, 1).toDouble();
5184             rePos.normalizedY = token.section(QLatin1Char(':'), 2, 2).toDouble();
5185             rePos.pos = Center;
5186         } else if (token.startsWith(QLatin1String("C2"))) {
5187             rePos.enabled = true;
5188             rePos.normalizedX = token.section(QLatin1Char(':'), 1, 1).toDouble();
5189             rePos.normalizedY = token.section(QLatin1Char(':'), 2, 2).toDouble();
5190             if (token.section(QLatin1Char(':'), 3, 3).toInt() == 1)
5191                 rePos.pos = Center;
5192             else
5193                 rePos.pos = TopLeft;
5194         } else if (token.startsWith(QLatin1String("AF1"))) {
5195             autoFit.enabled = true;
5196             autoFit.width = token.section(QLatin1Char(':'), 1, 1) == QLatin1String("T");
5197             autoFit.height = token.section(QLatin1Char(':'), 2, 2) == QLatin1String("T");
5198         }
5199         // proceed tokenizing string
5200         field++;
5201         token = xmlDesc.section(QLatin1Char(';'), field, field);
5202     }
5203 }
5204 
toString() const5205 QString DocumentViewport::toString() const
5206 {
5207     // start string with page number
5208     QString s = QString::number(pageNumber);
5209     // if has center coordinates, save them on string
5210     if (rePos.enabled)
5211         s += QStringLiteral(";C2:") + QString::number(rePos.normalizedX) + QLatin1Char(':') + QString::number(rePos.normalizedY) + QLatin1Char(':') + QString::number(rePos.pos);
5212     // if has autofit enabled, save its state on string
5213     if (autoFit.enabled)
5214         s += QStringLiteral(";AF1:") + (autoFit.width ? QLatin1Char('T') : QLatin1Char('F')) + QLatin1Char(':') + (autoFit.height ? QLatin1Char('T') : QLatin1Char('F'));
5215     return s;
5216 }
5217 
isValid() const5218 bool DocumentViewport::isValid() const
5219 {
5220     return pageNumber >= 0;
5221 }
5222 
operator ==(const DocumentViewport & other) const5223 bool DocumentViewport::operator==(const DocumentViewport &other) const
5224 {
5225     bool equal = (pageNumber == other.pageNumber) && (rePos.enabled == other.rePos.enabled) && (autoFit.enabled == other.autoFit.enabled);
5226     if (!equal)
5227         return false;
5228     if (rePos.enabled && ((rePos.normalizedX != other.rePos.normalizedX) || (rePos.normalizedY != other.rePos.normalizedY) || rePos.pos != other.rePos.pos))
5229         return false;
5230     if (autoFit.enabled && ((autoFit.width != other.autoFit.width) || (autoFit.height != other.autoFit.height)))
5231         return false;
5232     return true;
5233 }
5234 
operator <(const DocumentViewport & other) const5235 bool DocumentViewport::operator<(const DocumentViewport &other) const
5236 {
5237     // TODO: Check autoFit and Position
5238 
5239     if (pageNumber != other.pageNumber)
5240         return pageNumber < other.pageNumber;
5241 
5242     if (!rePos.enabled && other.rePos.enabled)
5243         return true;
5244 
5245     if (!other.rePos.enabled)
5246         return false;
5247 
5248     if (rePos.normalizedY != other.rePos.normalizedY)
5249         return rePos.normalizedY < other.rePos.normalizedY;
5250 
5251     return rePos.normalizedX < other.rePos.normalizedX;
5252 }
5253 
5254 /** DocumentInfo **/
5255 
DocumentInfo()5256 DocumentInfo::DocumentInfo()
5257     : d(new DocumentInfoPrivate())
5258 {
5259 }
5260 
DocumentInfo(const DocumentInfo & info)5261 DocumentInfo::DocumentInfo(const DocumentInfo &info)
5262     : d(new DocumentInfoPrivate())
5263 {
5264     *this = info;
5265 }
5266 
operator =(const DocumentInfo & info)5267 DocumentInfo &DocumentInfo::operator=(const DocumentInfo &info)
5268 {
5269     if (this != &info) {
5270         d->values = info.d->values;
5271         d->titles = info.d->titles;
5272     }
5273     return *this;
5274 }
5275 
~DocumentInfo()5276 DocumentInfo::~DocumentInfo()
5277 {
5278     delete d;
5279 }
5280 
set(const QString & key,const QString & value,const QString & title)5281 void DocumentInfo::set(const QString &key, const QString &value, const QString &title)
5282 {
5283     d->values[key] = value;
5284     d->titles[key] = title;
5285 }
5286 
set(Key key,const QString & value)5287 void DocumentInfo::set(Key key, const QString &value)
5288 {
5289     d->values[getKeyString(key)] = value;
5290 }
5291 
keys() const5292 QStringList DocumentInfo::keys() const
5293 {
5294     return d->values.keys();
5295 }
5296 
get(Key key) const5297 QString DocumentInfo::get(Key key) const
5298 {
5299     return get(getKeyString(key));
5300 }
5301 
get(const QString & key) const5302 QString DocumentInfo::get(const QString &key) const
5303 {
5304     return d->values[key];
5305 }
5306 
getKeyString(Key key)5307 QString DocumentInfo::getKeyString(Key key) // const
5308 {
5309     switch (key) {
5310     case Title:
5311         return QStringLiteral("title");
5312         break;
5313     case Subject:
5314         return QStringLiteral("subject");
5315         break;
5316     case Description:
5317         return QStringLiteral("description");
5318         break;
5319     case Author:
5320         return QStringLiteral("author");
5321         break;
5322     case Creator:
5323         return QStringLiteral("creator");
5324         break;
5325     case Producer:
5326         return QStringLiteral("producer");
5327         break;
5328     case Copyright:
5329         return QStringLiteral("copyright");
5330         break;
5331     case Pages:
5332         return QStringLiteral("pages");
5333         break;
5334     case CreationDate:
5335         return QStringLiteral("creationDate");
5336         break;
5337     case ModificationDate:
5338         return QStringLiteral("modificationDate");
5339         break;
5340     case MimeType:
5341         return QStringLiteral("mimeType");
5342         break;
5343     case Category:
5344         return QStringLiteral("category");
5345         break;
5346     case Keywords:
5347         return QStringLiteral("keywords");
5348         break;
5349     case FilePath:
5350         return QStringLiteral("filePath");
5351         break;
5352     case DocumentSize:
5353         return QStringLiteral("documentSize");
5354         break;
5355     case PagesSize:
5356         return QStringLiteral("pageSize");
5357         break;
5358     default:
5359         qCWarning(OkularCoreDebug) << "Unknown" << key;
5360         return QString();
5361         break;
5362     }
5363 }
5364 
getKeyFromString(const QString & key)5365 DocumentInfo::Key DocumentInfo::getKeyFromString(const QString &key) // const
5366 {
5367     if (key == QLatin1String("title"))
5368         return Title;
5369     else if (key == QLatin1String("subject"))
5370         return Subject;
5371     else if (key == QLatin1String("description"))
5372         return Description;
5373     else if (key == QLatin1String("author"))
5374         return Author;
5375     else if (key == QLatin1String("creator"))
5376         return Creator;
5377     else if (key == QLatin1String("producer"))
5378         return Producer;
5379     else if (key == QLatin1String("copyright"))
5380         return Copyright;
5381     else if (key == QLatin1String("pages"))
5382         return Pages;
5383     else if (key == QLatin1String("creationDate"))
5384         return CreationDate;
5385     else if (key == QLatin1String("modificationDate"))
5386         return ModificationDate;
5387     else if (key == QLatin1String("mimeType"))
5388         return MimeType;
5389     else if (key == QLatin1String("category"))
5390         return Category;
5391     else if (key == QLatin1String("keywords"))
5392         return Keywords;
5393     else if (key == QLatin1String("filePath"))
5394         return FilePath;
5395     else if (key == QLatin1String("documentSize"))
5396         return DocumentSize;
5397     else if (key == QLatin1String("pageSize"))
5398         return PagesSize;
5399     else
5400         return Invalid;
5401 }
5402 
getKeyTitle(Key key)5403 QString DocumentInfo::getKeyTitle(Key key) // const
5404 {
5405     switch (key) {
5406     case Title:
5407         return i18n("Title");
5408         break;
5409     case Subject:
5410         return i18n("Subject");
5411         break;
5412     case Description:
5413         return i18n("Description");
5414         break;
5415     case Author:
5416         return i18n("Author");
5417         break;
5418     case Creator:
5419         return i18n("Creator");
5420         break;
5421     case Producer:
5422         return i18n("Producer");
5423         break;
5424     case Copyright:
5425         return i18n("Copyright");
5426         break;
5427     case Pages:
5428         return i18n("Pages");
5429         break;
5430     case CreationDate:
5431         return i18n("Created");
5432         break;
5433     case ModificationDate:
5434         return i18n("Modified");
5435         break;
5436     case MimeType:
5437         return i18n("MIME Type");
5438         break;
5439     case Category:
5440         return i18n("Category");
5441         break;
5442     case Keywords:
5443         return i18n("Keywords");
5444         break;
5445     case FilePath:
5446         return i18n("File Path");
5447         break;
5448     case DocumentSize:
5449         return i18n("File Size");
5450         break;
5451     case PagesSize:
5452         return i18n("Page Size");
5453         break;
5454     default:
5455         return QString();
5456         break;
5457     }
5458 }
5459 
getKeyTitle(const QString & key) const5460 QString DocumentInfo::getKeyTitle(const QString &key) const
5461 {
5462     QString title = getKeyTitle(getKeyFromString(key));
5463     if (title.isEmpty())
5464         title = d->titles[key];
5465     return title;
5466 }
5467 
5468 /** DocumentSynopsis **/
5469 
DocumentSynopsis()5470 DocumentSynopsis::DocumentSynopsis()
5471     : QDomDocument(QStringLiteral("DocumentSynopsis"))
5472 {
5473     // void implementation, only subclassed for naming
5474 }
5475 
DocumentSynopsis(const QDomDocument & document)5476 DocumentSynopsis::DocumentSynopsis(const QDomDocument &document)
5477     : QDomDocument(document)
5478 {
5479 }
5480 
5481 /** EmbeddedFile **/
5482 
EmbeddedFile()5483 EmbeddedFile::EmbeddedFile()
5484 {
5485 }
5486 
~EmbeddedFile()5487 EmbeddedFile::~EmbeddedFile()
5488 {
5489 }
5490 
VisiblePageRect(int page,const NormalizedRect & rectangle)5491 VisiblePageRect::VisiblePageRect(int page, const NormalizedRect &rectangle)
5492     : pageNumber(page)
5493     , rect(rectangle)
5494 {
5495 }
5496 
5497 /** NewSignatureData **/
5498 
5499 struct Okular::NewSignatureDataPrivate {
5500     NewSignatureDataPrivate() = default;
5501 
5502     QString certNickname;
5503     QString certSubjectCommonName;
5504     QString password;
5505     int page;
5506     NormalizedRect boundingRectangle;
5507 };
5508 
NewSignatureData()5509 NewSignatureData::NewSignatureData()
5510     : d(new NewSignatureDataPrivate())
5511 {
5512 }
5513 
~NewSignatureData()5514 NewSignatureData::~NewSignatureData()
5515 {
5516     delete d;
5517 }
5518 
certNickname() const5519 QString NewSignatureData::certNickname() const
5520 {
5521     return d->certNickname;
5522 }
5523 
setCertNickname(const QString & certNickname)5524 void NewSignatureData::setCertNickname(const QString &certNickname)
5525 {
5526     d->certNickname = certNickname;
5527 }
5528 
certSubjectCommonName() const5529 QString NewSignatureData::certSubjectCommonName() const
5530 {
5531     return d->certSubjectCommonName;
5532 }
5533 
setCertSubjectCommonName(const QString & certSubjectCommonName)5534 void NewSignatureData::setCertSubjectCommonName(const QString &certSubjectCommonName)
5535 {
5536     d->certSubjectCommonName = certSubjectCommonName;
5537 }
5538 
password() const5539 QString NewSignatureData::password() const
5540 {
5541     return d->password;
5542 }
5543 
setPassword(const QString & password)5544 void NewSignatureData::setPassword(const QString &password)
5545 {
5546     d->password = password;
5547 }
5548 
page() const5549 int NewSignatureData::page() const
5550 {
5551     return d->page;
5552 }
5553 
setPage(int page)5554 void NewSignatureData::setPage(int page)
5555 {
5556     d->page = page;
5557 }
5558 
boundingRectangle() const5559 NormalizedRect NewSignatureData::boundingRectangle() const
5560 {
5561     return d->boundingRectangle;
5562 }
5563 
setBoundingRectangle(const NormalizedRect & rect)5564 void NewSignatureData::setBoundingRectangle(const NormalizedRect &rect)
5565 {
5566     d->boundingRectangle = rect;
5567 }
5568 
5569 #undef foreachObserver
5570 #undef foreachObserverD
5571 
5572 #include "document.moc"
5573 
5574 /* kate: replace-tabs on; indent-width 4; */
5575