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