Okular

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