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