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 std::shared_ptr<Event> event;
1209 QString oldVal;
1210 if (dynamic_cast<FormFieldText *>(form) || dynamic_cast<FormFieldChoice *>(form)) {
1211 // Prepare text calculate event
1212 event = Event::createFormCalculateEvent(form, m_pagesVector[pageIdx]);
1213 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
1214 executeScriptEvent(event, linkscript);
1215 // The value maybe changed in javascript so save it first.
1216 oldVal = form->value().toString();
1217
1218 if (event) {
1219 // Update text field from calculate
1220 const QString newVal = event->value().toString();
1221 if (newVal != oldVal) {
1222 form->setValue(QVariant(newVal));
1223 form->setAppearanceValue(QVariant(newVal));
1224 bool returnCode = true;
1225 if (form->additionalAction(Okular::FormField::FieldModified) && !form->isReadOnly()) {
1226 m_parent->processKeystrokeCommitAction(form->additionalAction(Okular::FormField::FieldModified), form, returnCode);
1227 }
1228 if (const Okular::Action *action = form->additionalAction(Okular::FormField::ValidateField)) {
1229 if (returnCode) {
1230 m_parent->processValidateAction(action, form, returnCode);
1231 }
1232 }
1233 if (!returnCode) {
1234 continue;
1235 } else {
1236 form->commitValue(form->value().toString());
1237 }
1238 if (const Okular::Action *action = form->additionalAction(Okular::FormField::FormatField)) {
1239 // The format action handles the refresh.
1240 m_parent->processFormatAction(action, form);
1241 } else {
1242 form->commitFormattedValue(form->value().toString());
1243 Q_EMIT m_parent->refreshFormWidget(form);
1244 pageNeedsRefresh = true;
1245 }
1246 }
1247 }
1248 }
1249 } else {
1250 qWarning() << "Form that is part of calculate order doesn't have a calculate action";
1251 }
1252 }
1253 }
1254 if (pageNeedsRefresh) {
1255 refreshPixmaps(p->number());
1256 }
1257 }
1258 }
1259 }
1260}
1261
1262void DocumentPrivate::saveDocumentInfo() const
1263{
1264 if (m_xmlFileName.isEmpty()) {
1265 return;
1266 }
1267
1268 QFile infoFile(m_xmlFileName);
1269 qCDebug(OkularCoreDebug) << "About to save document info to" << m_xmlFileName;
1271 qCWarning(OkularCoreDebug) << "Failed to open docdata file" << m_xmlFileName;
1272 return;
1273 }
1274 // 1. Create DOM
1275 QDomDocument doc(QStringLiteral("documentInfo"));
1276 QDomProcessingInstruction xmlPi = doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
1277 doc.appendChild(xmlPi);
1278 QDomElement root = doc.createElement(QStringLiteral("documentInfo"));
1279 root.setAttribute(QStringLiteral("url"), m_url.toDisplayString(QUrl::PreferLocalFile));
1280 doc.appendChild(root);
1281
1282 // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM
1283 // -> do this if there are not-yet-migrated annots or forms in docdata/
1284 if (m_docdataMigrationNeeded) {
1285 QDomElement pageList = doc.createElement(QStringLiteral("pageList"));
1286 root.appendChild(pageList);
1287 // OriginalAnnotationPageItems and OriginalFormFieldPageItems tell to
1288 // store the same unmodified annotation list and form contents that we
1289 // read when we opened the file and ignore any change made by the user.
1290 // Since we don't store annotations and forms in docdata/ any more, this is
1291 // necessary to preserve annotations/forms that previous Okular version
1292 // had stored there.
1293 const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems | OriginalFormFieldPageItems;
1294 // <page list><page number='x'>.... </page> save pages that hold data
1295 QVector<Page *>::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd();
1296 for (; pIt != pEnd; ++pIt) {
1297 (*pIt)->d->saveLocalContents(pageList, doc, saveWhat);
1298 }
1299 }
1300
1301 // 2.2. Save document info (current viewport, history, ... ) to DOM
1302 QDomElement generalInfo = doc.createElement(QStringLiteral("generalInfo"));
1303 root.appendChild(generalInfo);
1304 // create rotation node
1305 if (m_rotation != Rotation0) {
1306 QDomElement rotationNode = doc.createElement(QStringLiteral("rotation"));
1307 generalInfo.appendChild(rotationNode);
1308 rotationNode.appendChild(doc.createTextNode(QString::number((int)m_rotation)));
1309 }
1310 // <general info><history> ... </history> save history up to OKULAR_HISTORY_SAVEDSTEPS viewports
1311 const auto currentViewportIterator = std::list<DocumentViewport>::const_iterator(m_viewportIterator);
1312 std::list<DocumentViewport>::const_iterator backIterator = currentViewportIterator;
1313 if (backIterator != m_viewportHistory.end()) {
1314 // go back up to OKULAR_HISTORY_SAVEDSTEPS steps from the current viewportIterator
1315 int backSteps = OKULAR_HISTORY_SAVEDSTEPS;
1316 while (backSteps-- && backIterator != m_viewportHistory.begin()) {
1317 --backIterator;
1318 }
1319
1320 // create history root node
1321 QDomElement historyNode = doc.createElement(QStringLiteral("history"));
1322 generalInfo.appendChild(historyNode);
1323
1324 // add old[backIterator] and present[viewportIterator] items
1325 std::list<DocumentViewport>::const_iterator endIt = currentViewportIterator;
1326 ++endIt;
1327 while (backIterator != endIt) {
1328 QString name = (backIterator == currentViewportIterator) ? QStringLiteral("current") : QStringLiteral("oldPage");
1329 QDomElement historyEntry = doc.createElement(name);
1330 historyEntry.setAttribute(QStringLiteral("viewport"), (*backIterator).toString());
1331 historyNode.appendChild(historyEntry);
1332 ++backIterator;
1333 }
1334 }
1335 // create views root node
1336 QDomElement viewsNode = doc.createElement(QStringLiteral("views"));
1337 generalInfo.appendChild(viewsNode);
1338 for (View *view : std::as_const(m_views)) {
1339 QDomElement viewEntry = doc.createElement(QStringLiteral("view"));
1340 viewEntry.setAttribute(QStringLiteral("name"), view->name());
1341 viewsNode.appendChild(viewEntry);
1342 saveViewsInfo(view, viewEntry);
1343 }
1344
1345 // 3. Save DOM to XML file
1346 QString xml = doc.toString();
1347 QTextStream os(&infoFile);
1348 os.setEncoding(QStringConverter::Utf8);
1349 os << xml;
1350 infoFile.close();
1351}
1352
1353void DocumentPrivate::slotTimedMemoryCheck()
1354{
1355 // [MEM] clean memory (for 'free mem dependent' profiles only)
1356 if (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Low && m_allocatedPixmapsTotalMemory > 1024 * 1024) {
1357 cleanupPixmapMemory();
1358 }
1359}
1360
1361void DocumentPrivate::sendGeneratorPixmapRequest()
1362{
1363 /* If the pixmap cache will have to be cleaned in order to make room for the
1364 * next request, get the distance from the current viewport of the page
1365 * whose pixmap will be removed. We will ignore preload requests for pages
1366 * that are at the same distance or farther */
1367 const qulonglong memoryToFree = calculateMemoryToFree();
1368 const int currentViewportPage = (*m_viewportIterator).pageNumber;
1369 int maxDistance = INT_MAX; // Default: No maximum
1370 if (memoryToFree) {
1371 const AllocatedPixmap *pixmapToReplace = searchLowestPriorityPixmap(true);
1372 if (pixmapToReplace) {
1373 maxDistance = qAbs(pixmapToReplace->page - currentViewportPage);
1374 }
1375 }
1376
1377 // find a request
1378 PixmapRequest *request = nullptr;
1379 m_pixmapRequestsMutex.lock();
1380 while (!m_pixmapRequestsStack.empty() && !request) {
1381 PixmapRequest *r = m_pixmapRequestsStack.back();
1382 if (!r) {
1383 m_pixmapRequestsStack.pop_back();
1384 continue;
1385 }
1386
1387 QRect requestRect = r->isTile() ? r->normalizedRect().geometry(r->width(), r->height()) : QRect(0, 0, r->width(), r->height());
1388 TilesManager *tilesManager = r->d->tilesManager();
1389 const double normalizedArea = r->normalizedRect().width() * r->normalizedRect().height();
1390 const QScreen *screen = nullptr;
1391 if (m_widget) {
1392 const QWindow *window = m_widget->window()->windowHandle();
1393 if (window) {
1394 screen = window->screen();
1395 }
1396 }
1397 if (!screen) {
1399 }
1400 const long screenSize = screen->devicePixelRatio() * screen->size().width() * screen->devicePixelRatio() * screen->size().height();
1401
1402 // Make sure the page is the right size to receive the pixmap
1403 r->page()->setPageSize(r->observer(), r->width(), r->height());
1404
1405 // If it's a preload but the generator is not threaded no point in trying to preload
1406 if (r->preload() && !m_generator->hasFeature(Generator::Threaded)) {
1407 m_pixmapRequestsStack.pop_back();
1408 delete r;
1409 }
1410 // request only if page isn't already present and request has valid id
1411 else if ((!r->d->mForce && r->page()->hasPixmap(r->observer(), r->width(), r->height(), r->normalizedRect())) || !m_observers.contains(r->observer())) {
1412 m_pixmapRequestsStack.pop_back();
1413 delete r;
1414 } else if (!r->d->mForce && r->preload() && qAbs(r->pageNumber() - currentViewportPage) >= maxDistance) {
1415 m_pixmapRequestsStack.pop_back();
1416 // qCDebug(OkularCoreDebug) << "Ignoring request that doesn't fit in cache";
1417 delete r;
1418 }
1419 // Ignore requests for pixmaps that are already being generated
1420 else if (tilesManager && tilesManager->isRequesting(r->normalizedRect(), r->width(), r->height())) {
1421 m_pixmapRequestsStack.pop_back();
1422 delete r;
1423 }
1424 // If the requested area is above 4*screenSize pixels, and we're not rendering most of the page, switch on the tile manager
1425 else if (!tilesManager && m_generator->hasFeature(Generator::TiledRendering) && (long)r->width() * (long)r->height() > 4L * screenSize && normalizedArea < 0.75) {
1426 // if the image is too big. start using tiles
1427 qCDebug(OkularCoreDebug).nospace() << "Start using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1428
1429 // fill the tiles manager with the last rendered pixmap
1430 const QPixmap *pixmap = r->page()->_o_nearestPixmap(r->observer(), r->width(), r->height());
1431 if (pixmap) {
1432 tilesManager = new TilesManager(r->pageNumber(), pixmap->width(), pixmap->height(), r->page()->rotation());
1433 tilesManager->setPixmap(pixmap, NormalizedRect(0, 0, 1, 1), true /*isPartialPixmap*/);
1434 tilesManager->setSize(r->width(), r->height());
1435 } else {
1436 // create new tiles manager
1437 tilesManager = new TilesManager(r->pageNumber(), r->width(), r->height(), r->page()->rotation());
1438 }
1439 tilesManager->setRequest(r->normalizedRect(), r->width(), r->height());
1440 r->page()->deletePixmap(r->observer());
1441 r->page()->d->setTilesManager(r->observer(), tilesManager);
1442 r->setTile(true);
1443
1444 // Change normalizedRect to the smallest rect that contains all
1445 // visible tiles.
1446 if (!r->normalizedRect().isNull()) {
1447 NormalizedRect tilesRect;
1448 const QList<Tile> tiles = tilesManager->tilesAt(r->normalizedRect(), TilesManager::TerminalTile);
1449 QList<Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
1450 while (tIt != tEnd) {
1451 Tile tile = *tIt;
1452 if (tilesRect.isNull()) {
1453 tilesRect = tile.rect();
1454 } else {
1455 tilesRect |= tile.rect();
1456 }
1457
1458 ++tIt;
1459 }
1460
1461 r->setNormalizedRect(tilesRect);
1462 request = r;
1463 } else {
1464 // Discard request if normalizedRect is null. This happens in
1465 // preload requests issued by PageView if the requested page is
1466 // not visible and the user has just switched from a non-tiled
1467 // zoom level to a tiled one
1468 m_pixmapRequestsStack.pop_back();
1469 delete r;
1470 }
1471 }
1472 // If the requested area is below 3*screenSize pixels, switch off the tile manager
1473 else if (tilesManager && (long)r->width() * (long)r->height() < 3L * screenSize) {
1474 qCDebug(OkularCoreDebug).nospace() << "Stop using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1475
1476 // page is too small. stop using tiles.
1477 r->page()->deletePixmap(r->observer());
1478 r->setTile(false);
1479
1480 request = r;
1481 } else if ((long)requestRect.width() * (long)requestRect.height() > 100L * screenSize && (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Greedy)) {
1482 m_pixmapRequestsStack.pop_back();
1483 if (!m_warnedOutOfMemory) {
1484 qCWarning(OkularCoreDebug).nospace() << "Running out of memory on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1485 qCWarning(OkularCoreDebug) << "this message will be reported only once.";
1486 m_warnedOutOfMemory = true;
1487 }
1488 delete r;
1489 } else {
1490 request = r;
1491 }
1492 }
1493
1494 // if no request found (or already generated), return
1495 if (!request) {
1496 m_pixmapRequestsMutex.unlock();
1497 return;
1498 }
1499
1500 // [MEM] preventive memory freeing
1501 qulonglong pixmapBytes = 0;
1502 TilesManager *tm = request->d->tilesManager();
1503 if (tm) {
1504 pixmapBytes = tm->totalMemory();
1505 } else {
1506 pixmapBytes = 4 * request->width() * request->height();
1507 }
1508
1509 if (pixmapBytes > (1024 * 1024)) {
1510 cleanupPixmapMemory(memoryToFree /* previously calculated value */);
1511 }
1512
1513 // submit the request to the generator
1514 if (m_generator->canGeneratePixmap()) {
1515 QRect requestRect = !request->isTile() ? QRect(0, 0, request->width(), request->height()) : request->normalizedRect().geometry(request->width(), request->height());
1516 qCDebug(OkularCoreDebug).nospace() << "sending request observer=" << request->observer() << " " << requestRect.width() << "x" << requestRect.height() << "@" << request->pageNumber() << " async == " << request->asynchronous()
1517 << " isTile == " << request->isTile();
1518 m_pixmapRequestsStack.remove(request);
1519
1520 if (tm) {
1521 tm->setRequest(request->normalizedRect(), request->width(), request->height());
1522 }
1523
1524 if ((int)m_rotation % 2) {
1525 request->d->swap();
1526 }
1527
1528 if (m_rotation != Rotation0 && !request->normalizedRect().isNull()) {
1529 request->setNormalizedRect(TilesManager::fromRotatedRect(request->normalizedRect(), m_rotation));
1530 }
1531
1532 // If set elsewhere we already know we want it to be partial
1533 if (!request->partialUpdatesWanted()) {
1534 request->setPartialUpdatesWanted(request->asynchronous() && !request->page()->hasPixmap(request->observer()));
1535 }
1536
1537 // we always have to unlock _before_ the generatePixmap() because
1538 // a sync generation would end with requestDone() -> deadlock, and
1539 // we can not really know if the generator can do async requests
1540 m_executingPixmapRequests.push_back(request);
1541 m_pixmapRequestsMutex.unlock();
1542 m_generator->generatePixmap(request);
1543 } else {
1544 m_pixmapRequestsMutex.unlock();
1545 // pino (7/4/2006): set the polling interval from 10 to 30
1546 QTimer::singleShot(30, m_parent, [this] { sendGeneratorPixmapRequest(); });
1547 }
1548}
1549
1550void DocumentPrivate::rotationFinished(int page, Okular::Page *okularPage)
1551{
1552 const Okular::Page *wantedPage = m_pagesVector.value(page, nullptr);
1553 if (!wantedPage || wantedPage != okularPage) {
1554 return;
1555 }
1556
1557 for (DocumentObserver *o : std::as_const(m_observers)) {
1558 o->notifyPageChanged(page, DocumentObserver::Pixmap | DocumentObserver::Annotations);
1559 }
1560}
1561
1562void DocumentPrivate::slotFontReadingProgress(int page)
1563{
1564 Q_EMIT m_parent->fontReadingProgress(page);
1565
1566 if (page >= (int)m_parent->pages() - 1) {
1567 Q_EMIT m_parent->fontReadingEnded();
1568 m_fontThread = nullptr;
1569 m_fontsCached = true;
1570 }
1571}
1572
1573void DocumentPrivate::fontReadingGotFont(const Okular::FontInfo &font)
1574{
1575 // Try to avoid duplicate fonts
1576 if (m_fontsCache.indexOf(font) == -1) {
1577 m_fontsCache.append(font);
1578
1579 Q_EMIT m_parent->gotFont(font);
1580 }
1581}
1582
1583void DocumentPrivate::slotGeneratorConfigChanged()
1584{
1585 if (!m_generator) {
1586 return;
1587 }
1588
1589 // reparse generator config and if something changed clear Pages
1590 bool configchanged = false;
1591 QHash<QString, GeneratorInfo>::iterator it = m_loadedGenerators.begin(), itEnd = m_loadedGenerators.end();
1592 for (; it != itEnd; ++it) {
1593 Okular::ConfigInterface *iface = generatorConfig(it.value());
1594 if (iface) {
1595 bool it_changed = iface->reparseConfig();
1596 if (it_changed && (m_generator == it.value().generator)) {
1597 configchanged = true;
1598 }
1599 }
1600 }
1601 if (configchanged) {
1602 // invalidate pixmaps
1603 QVector<Page *>::const_iterator it = m_pagesVector.constBegin(), end = m_pagesVector.constEnd();
1604 for (; it != end; ++it) {
1605 (*it)->deletePixmaps();
1606 }
1607
1608 // [MEM] remove allocation descriptors
1609 qDeleteAll(m_allocatedPixmaps);
1610 m_allocatedPixmaps.clear();
1611 m_allocatedPixmapsTotalMemory = 0;
1612
1613 // send reload signals to observers
1614 foreachObserverD(notifyContentsCleared(DocumentObserver::Pixmap));
1615 }
1616
1617 // free memory if in 'low' profile
1618 if (SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !m_allocatedPixmaps.empty() && !m_pagesVector.isEmpty()) {
1619 cleanupPixmapMemory();
1620 }
1621}
1622
1623void DocumentPrivate::refreshPixmaps(int pageNumber)
1624{
1625 Page *page = m_pagesVector.value(pageNumber, nullptr);
1626 if (!page) {
1627 return;
1628 }
1629
1630 QMap<DocumentObserver *, PagePrivate::PixmapObject>::ConstIterator it = page->d->m_pixmaps.constBegin(), itEnd = page->d->m_pixmaps.constEnd();
1631 QVector<Okular::PixmapRequest *> pixmapsToRequest;
1632 for (; it != itEnd; ++it) {
1633 const QSize size = (*it).m_pixmap->size();
1634 PixmapRequest *p = new PixmapRequest(it.key(), pageNumber, size.width(), size.height(), 1 /* dpr */, 1, PixmapRequest::Asynchronous);
1635 p->d->mForce = true;
1636 pixmapsToRequest << p;
1637 }
1638
1639 // Need to do this ↑↓ in two steps since requestPixmaps can end up calling cancelRenderingBecauseOf
1640 // which changes m_pixmaps and thus breaks the loop above
1641 for (PixmapRequest *pr : std::as_const(pixmapsToRequest)) {
1642 QList<Okular::PixmapRequest *> requestedPixmaps;
1643 requestedPixmaps.push_back(pr);
1644 m_parent->requestPixmaps(requestedPixmaps, Okular::Document::NoOption);
1645 }
1646
1647 for (DocumentObserver *observer : std::as_const(m_observers)) {
1648 QList<Okular::PixmapRequest *> requestedPixmaps;
1649
1650 TilesManager *tilesManager = page->d->tilesManager(observer);
1651 if (tilesManager) {
1652 tilesManager->markDirty();
1653
1654 PixmapRequest *p = new PixmapRequest(observer, pageNumber, tilesManager->width(), tilesManager->height(), 1 /* dpr */, 1, PixmapRequest::Asynchronous);
1655
1656 // Get the visible page rect
1657 NormalizedRect visibleRect;
1658 QVector<Okular::VisiblePageRect *>::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd();
1659 for (; vIt != vEnd; ++vIt) {
1660 if ((*vIt)->pageNumber == pageNumber) {
1661 visibleRect = (*vIt)->rect;
1662 break;
1663 }
1664 }
1665
1666 if (!visibleRect.isNull()) {
1667 p->setNormalizedRect(visibleRect);
1668 p->setTile(true);
1669 p->d->mForce = true;
1670 requestedPixmaps.push_back(p);
1671 } else {
1672 delete p;
1673 }
1674 }
1675
1676 m_parent->requestPixmaps(requestedPixmaps, Okular::Document::NoOption);
1677 }
1678}
1679
1680void DocumentPrivate::_o_configChanged()
1681{
1682 // free text pages if needed
1683 calculateMaxTextPages();
1684 while (m_allocatedTextPagesFifo.count() > m_maxAllocatedTextPages) {
1685 int pageToKick = m_allocatedTextPagesFifo.takeFirst();
1686 m_pagesVector.at(pageToKick)->setTextPage(nullptr); // deletes the textpage
1687 }
1688}
1689
1690void DocumentPrivate::doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct)
1691{
1692 DoContinueDirectionMatchSearchStruct *searchStruct = static_cast<DoContinueDirectionMatchSearchStruct *>(doContinueDirectionMatchSearchStruct);
1693 RunningSearch *search = m_searches.value(searchStruct->searchID);
1694
1695 if ((m_searchCancelled && !searchStruct->match) || !search) {
1696 // if the user cancelled but he just got a match, give him the match!
1698
1699 if (search) {
1700 search->isCurrentlySearching = false;
1701 }
1702
1703 Q_EMIT m_parent->searchFinished(searchStruct->searchID, Document::SearchCancelled);
1704 delete searchStruct->pagesToNotify;
1705 delete searchStruct;
1706 return;
1707 }
1708
1709 const bool forward = search->cachedType == Document::NextMatch;
1710 bool doContinue = false;
1711 // if no match found, loop through the whole doc, starting from currentPage
1712 if (!searchStruct->match) {
1713 const int pageCount = m_pagesVector.count();
1714 if (search->pagesDone < pageCount) {
1715 doContinue = true;
1716 if (searchStruct->currentPage >= pageCount) {
1717 searchStruct->currentPage = 0;
1718 Q_EMIT m_parent->notice(i18n("Continuing search from beginning"), 3000);
1719 } else if (searchStruct->currentPage < 0) {
1720 searchStruct->currentPage = pageCount - 1;
1721 Q_EMIT m_parent->notice(i18n("Continuing search from bottom"), 3000);
1722 }
1723 }
1724 }
1725
1726 if (doContinue) {
1727 // get page
1728 const Page *page = m_pagesVector[searchStruct->currentPage];
1729 // request search page if needed
1730 if (!page->hasTextPage()) {
1731 m_parent->requestTextPage(page->number());
1732 }
1733
1734 // if found a match on the current page, end the loop
1735 searchStruct->match = page->findText(searchStruct->searchID, search->cachedString, forward ? FromTop : FromBottom, search->cachedCaseSensitivity);
1736 if (!searchStruct->match) {
1737 if (forward) {
1738 searchStruct->currentPage++;
1739 } else {
1740 searchStruct->currentPage--;
1741 }
1742 search->pagesDone++;
1743 } else {
1744 search->pagesDone = 1;
1745 }
1746
1747 // Both of the previous if branches need to call doContinueDirectionMatchSearch
1748 QTimer::singleShot(0, m_parent, [this, searchStruct] { doContinueDirectionMatchSearch(searchStruct); });
1749 } else {
1750 doProcessSearchMatch(searchStruct->match, search, searchStruct->pagesToNotify, searchStruct->currentPage, searchStruct->searchID, search->cachedViewportMove, search->cachedColor);
1751 delete searchStruct;
1752 }
1753}
1754
1755void DocumentPrivate::doProcessSearchMatch(RegularAreaRect *match, RunningSearch *search, QSet<int> *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor &color)
1756{
1757 // reset cursor to previous shape
1759
1760 bool foundAMatch = false;
1761
1762 search->isCurrentlySearching = false;
1763
1764 // if a match has been found..
1765 if (match) {
1766 // update the RunningSearch structure adding this match..
1767 foundAMatch = true;
1768 search->continueOnPage = currentPage;
1769 search->continueOnMatch = *match;
1770 search->highlightedPages.insert(currentPage);
1771 // ..add highlight to the page..
1772 m_pagesVector[currentPage]->d->setHighlight(searchID, match, color);
1773
1774 // ..queue page for notifying changes..
1775 pagesToNotify->insert(currentPage);
1776
1777 // Create a normalized rectangle around the search match that includes a 5% buffer on all sides.
1778 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);
1779
1780 const bool matchRectFullyVisible = isNormalizedRectangleFullyVisible(matchRectWithBuffer, currentPage);
1781
1782 // ..move the viewport to show the first of the searched word sequence centered
1783 if (moveViewport && !matchRectFullyVisible) {
1784 DocumentViewport searchViewport(currentPage);
1785 searchViewport.rePos.enabled = true;
1786 searchViewport.rePos.normalizedX = (match->first().left + match->first().right) / 2.0;
1787 searchViewport.rePos.normalizedY = (match->first().top + match->first().bottom) / 2.0;
1788 m_parent->setViewport(searchViewport, nullptr, true);
1789 }
1790 delete match;
1791 }
1792
1793 // notify observers about highlights changes
1794 for (int pageNumber : std::as_const(*pagesToNotify)) {
1795 for (DocumentObserver *observer : std::as_const(m_observers)) {
1796 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1797 }
1798 }
1799
1800 if (foundAMatch) {
1801 Q_EMIT m_parent->searchFinished(searchID, Document::MatchFound);
1802 } else {
1803 Q_EMIT m_parent->searchFinished(searchID, Document::NoMatchFound);
1804 }
1805
1806 delete pagesToNotify;
1807}
1808
1809void DocumentPrivate::doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID)
1810{
1811 QMap<Page *, QVector<RegularAreaRect *>> *pageMatches = static_cast<QMap<Page *, QVector<RegularAreaRect *>> *>(pageMatchesMap);
1812 QSet<int> *pagesToNotify = static_cast<QSet<int> *>(pagesToNotifySet);
1813 RunningSearch *search = m_searches.value(searchID);
1814
1815 if (m_searchCancelled || !search) {
1816 typedef QVector<RegularAreaRect *> MatchesVector;
1817
1819
1820 if (search) {
1821 search->isCurrentlySearching = false;
1822 }
1823
1824 Q_EMIT m_parent->searchFinished(searchID, Document::SearchCancelled);
1825 for (const MatchesVector &mv : std::as_const(*pageMatches)) {
1826 qDeleteAll(mv);
1827 }
1828 delete pageMatches;
1829 delete pagesToNotify;
1830 return;
1831 }
1832
1833 if (currentPage < m_pagesVector.count()) {
1834 // get page (from the first to the last)
1835 Page *page = m_pagesVector.at(currentPage);
1836 int pageNumber = page->number(); // redundant? is it == currentPage ?
1837
1838 // request search page if needed
1839 if (!page->hasTextPage()) {
1840 m_parent->requestTextPage(pageNumber);
1841 }
1842
1843 // loop on a page adding highlights for all found items
1844 RegularAreaRect *lastMatch = nullptr;
1845 while (true) {
1846 if (lastMatch) {
1847 lastMatch = page->findText(searchID, search->cachedString, NextResult, search->cachedCaseSensitivity, lastMatch);
1848 } else {
1849 lastMatch = page->findText(searchID, search->cachedString, FromTop, search->cachedCaseSensitivity);
1850 }
1851
1852 if (!lastMatch) {
1853 break;
1854 }
1855
1856 // add highlight rect to the matches map
1857 (*pageMatches)[page].append(lastMatch);
1858 }
1859 delete lastMatch;
1860
1861 QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID] { doContinueAllDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID); });
1862 } else {
1863 // reset cursor to previous shape
1865
1866 search->isCurrentlySearching = false;
1867 bool foundAMatch = pageMatches->count() != 0;
1868 QMap<Page *, QVector<RegularAreaRect *>>::const_iterator it, itEnd;
1869 it = pageMatches->constBegin();
1870 itEnd = pageMatches->constEnd();
1871 for (; it != itEnd; ++it) {
1872 for (RegularAreaRect *match : it.value()) {
1873 it.key()->d->setHighlight(searchID, match, search->cachedColor);
1874 delete match;
1875 }
1876 search->highlightedPages.insert(it.key()->number());
1877 pagesToNotify->insert(it.key()->number());
1878 }
1879
1880 for (DocumentObserver *observer : std::as_const(m_observers)) {
1881 observer->notifySetup(m_pagesVector, 0);
1882 }
1883
1884 // notify observers about highlights changes
1885 for (int pageNumber : std::as_const(*pagesToNotify)) {
1886 for (DocumentObserver *observer : std::as_const(m_observers)) {
1887 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1888 }
1889 }
1890
1891 if (foundAMatch) {
1892 Q_EMIT m_parent->searchFinished(searchID, Document::MatchFound);
1893 } else {
1894 Q_EMIT m_parent->searchFinished(searchID, Document::NoMatchFound);
1895 }
1896
1897 delete pageMatches;
1898 delete pagesToNotify;
1899 }
1900}
1901
1902void DocumentPrivate::doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList &words)
1903{
1904 typedef QPair<RegularAreaRect *, QColor> MatchColor;
1905 QMap<Page *, QVector<MatchColor>> *pageMatches = static_cast<QMap<Page *, QVector<MatchColor>> *>(pageMatchesMap);
1906 QSet<int> *pagesToNotify = static_cast<QSet<int> *>(pagesToNotifySet);
1907 RunningSearch *search = m_searches.value(searchID);
1908
1909 if (m_searchCancelled || !search) {
1910 typedef QVector<MatchColor> MatchesVector;
1911
1913
1914 if (search) {
1915 search->isCurrentlySearching = false;
1916 }
1917
1918 Q_EMIT m_parent->searchFinished(searchID, Document::SearchCancelled);
1919
1920 for (const MatchesVector &mv : std::as_const(*pageMatches)) {
1921 for (const MatchColor &mc : mv) {
1922 delete mc.first;
1923 }
1924 }
1925 delete pageMatches;
1926 delete pagesToNotify;
1927 return;
1928 }
1929
1930 const int wordCount = words.count();
1931 const int hueStep = (wordCount > 1) ? (60 / (wordCount - 1)) : 60;
1932 int baseHue, baseSat, baseVal;
1933 search->cachedColor.getHsv(&baseHue, &baseSat, &baseVal);
1934
1935 if (currentPage < m_pagesVector.count()) {
1936 // get page (from the first to the last)
1937 Page *page = m_pagesVector.at(currentPage);
1938 int pageNumber = page->number(); // redundant? is it == currentPage ?
1939
1940 // request search page if needed
1941 if (!page->hasTextPage()) {
1942 m_parent->requestTextPage(pageNumber);
1943 }
1944
1945 // loop on a page adding highlights for all found items
1946 bool allMatched = wordCount > 0, anyMatched = false;
1947 for (int w = 0; w < wordCount; w++) {
1948 const QString &word = words[w];
1949 int newHue = baseHue - w * hueStep;
1950 if (newHue < 0) {
1951 newHue += 360;
1952 }
1953 QColor wordColor = QColor::fromHsv(newHue, baseSat, baseVal);
1954 RegularAreaRect *lastMatch = nullptr;
1955 // add all highlights for current word
1956 bool wordMatched = false;
1957 while (true) {
1958 if (lastMatch) {
1959 lastMatch = page->findText(searchID, word, NextResult, search->cachedCaseSensitivity, lastMatch);
1960 } else {
1961 lastMatch = page->findText(searchID, word, FromTop, search->cachedCaseSensitivity);
1962 }
1963
1964 if (!lastMatch) {
1965 break;
1966 }
1967
1968 // add highligh rect to the matches map
1969 (*pageMatches)[page].append(MatchColor(lastMatch, wordColor));
1970 wordMatched = true;
1971 }
1972 allMatched = allMatched && wordMatched;
1973 anyMatched = anyMatched || wordMatched;
1974 }
1975
1976 // if not all words are present in page, remove partial highlights
1977 const bool matchAll = search->cachedType == Document::GoogleAll;
1978 if (!allMatched && matchAll) {
1979 const QVector<MatchColor> &matches = (*pageMatches)[page];
1980 for (const MatchColor &mc : matches) {
1981 delete mc.first;
1982 }
1983 pageMatches->remove(page);
1984 }
1985
1986 QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID, words] { doContinueGooglesDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID, words); });
1987 } else {
1988 // reset cursor to previous shape
1990
1991 search->isCurrentlySearching = false;
1992 bool foundAMatch = pageMatches->count() != 0;
1993 QMap<Page *, QVector<MatchColor>>::const_iterator it, itEnd;
1994 it = pageMatches->constBegin();
1995 itEnd = pageMatches->constEnd();
1996 for (; it != itEnd; ++it) {
1997 for (const MatchColor &mc : it.value()) {
1998 it.key()->d->setHighlight(searchID, mc.first, mc.second);
1999 delete mc.first;
2000 }
2001 search->highlightedPages.insert(it.key()->number());
2002 pagesToNotify->insert(it.key()->number());
2003 }
2004
2005 // send page lists to update observers (since some filter on bookmarks)
2006 for (DocumentObserver *observer : std::as_const(m_observers)) {
2007 observer->notifySetup(m_pagesVector, 0);
2008 }
2009
2010 // notify observers about highlights changes
2011 for (int pageNumber : std::as_const(*pagesToNotify)) {
2012 for (DocumentObserver *observer : std::as_const(m_observers)) {
2013 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
2014 }
2015 }
2016
2017 if (foundAMatch) {
2018 Q_EMIT m_parent->searchFinished(searchID, Document::MatchFound);
2019 } else {
2020 Q_EMIT m_parent->searchFinished(searchID, Document::NoMatchFound);
2021 }
2022
2023 delete pageMatches;
2024 delete pagesToNotify;
2025 }
2026}
2027
2028QVariant DocumentPrivate::documentMetaData(const Generator::DocumentMetaDataKey key, const QVariant &option) const
2029{
2030 switch (key) {
2032 bool giveDefault = option.toBool();
2033 QColor color;
2034 if ((SettingsCore::renderMode() == SettingsCore::EnumRenderMode::Paper) && SettingsCore::changeColors()) {
2035 color = SettingsCore::paperColor();
2036 } else if (giveDefault) {
2037 color = Qt::white;
2038 }
2039 return color;
2040 } break;
2041
2043 switch (SettingsCore::textAntialias()) {
2044 case SettingsCore::EnumTextAntialias::Enabled:
2045 return true;
2046 break;
2047 case SettingsCore::EnumTextAntialias::Disabled:
2048 return false;
2049 break;
2050 }
2051 break;
2052
2054 switch (SettingsCore::graphicsAntialias()) {
2055 case SettingsCore::EnumGraphicsAntialias::Enabled:
2056 return true;
2057 break;
2058 case SettingsCore::EnumGraphicsAntialias::Disabled:
2059 return false;
2060 break;
2061 }
2062 break;
2063
2065 switch (SettingsCore::textHinting()) {
2066 case SettingsCore::EnumTextHinting::Enabled:
2067 return true;
2068 break;
2069 case SettingsCore::EnumTextHinting::Disabled:
2070 return false;
2071 break;
2072 }
2073 break;
2074 }
2075 return QVariant();
2076}
2077
2078bool DocumentPrivate::isNormalizedRectangleFullyVisible(const Okular::NormalizedRect &rectOfInterest, int rectPage)
2079{
2080 bool rectFullyVisible = false;
2081 const QVector<Okular::VisiblePageRect *> &visibleRects = m_parent->visiblePageRects();
2084
2085 for (; (vIt != vEnd) && !rectFullyVisible; ++vIt) {
2086 if ((*vIt)->pageNumber == rectPage && (*vIt)->rect.contains(rectOfInterest.left, rectOfInterest.top) && (*vIt)->rect.contains(rectOfInterest.right, rectOfInterest.bottom)) {
2087 rectFullyVisible = true;
2088 }
2089 }
2090 return rectFullyVisible;
2091}
2092
2093struct pdfsyncpoint {
2094 QString file;
2095 qlonglong x;
2096 qlonglong y;
2097 int row;
2098 int column;
2099 int page;
2100};
2101
2102void DocumentPrivate::loadSyncFile(const QString &filePath)
2103{
2104 QFile f(filePath + QLatin1String("sync"));
2105 if (!f.open(QIODevice::ReadOnly)) {
2106 return;
2107 }
2108
2109 QTextStream ts(&f);
2110 // first row: core name of the pdf output
2111 const QString coreName = ts.readLine();
2112 // second row: version string, in the form 'Version %u'
2113 const QString versionstr = ts.readLine();
2114 // anchor the pattern with \A and \z to match the entire subject string
2115 // TODO: with Qt 5.12 QRegularExpression::anchoredPattern() can be used instead
2116 static QRegularExpression versionre(QStringLiteral("\\AVersion \\d+\\z"), QRegularExpression::CaseInsensitiveOption);
2117 QRegularExpressionMatch match = versionre.match(versionstr);
2118 if (!match.hasMatch()) {
2119 return;
2120 }
2121
2123 QStack<QString> fileStack;
2124 int currentpage = -1;
2125 const QLatin1String texStr(".tex");
2126 const QChar spaceChar = QChar::fromLatin1(' ');
2127
2128 fileStack.push(coreName + texStr);
2129
2130 const QSizeF dpi = m_generator->dpi();
2131
2132 QString line;
2133 while (!ts.atEnd()) {
2134 line = ts.readLine();
2135 const QStringList tokens = line.split(spaceChar, Qt::SkipEmptyParts);
2136 const int tokenSize = tokens.count();
2137 if (tokenSize < 1) {
2138 continue;
2139 }
2140 if (tokens.first() == QLatin1String("l") && tokenSize >= 3) {
2141 int id = tokens.at(1).toInt();
2143 if (it == points.constEnd()) {
2144 pdfsyncpoint pt;
2145 pt.x = 0;
2146 pt.y = 0;
2147 pt.row = tokens.at(2).toInt();
2148 pt.column = 0; // TODO
2149 pt.page = -1;
2150 pt.file = fileStack.top();
2151 points[id] = pt;
2152 }
2153 } else if (tokens.first() == QLatin1String("s") && tokenSize >= 2) {
2154 currentpage = tokens.at(1).toInt() - 1;
2155 } else if (tokens.first() == QLatin1String("p*") && tokenSize >= 4) {
2156 // TODO
2157 qCDebug(OkularCoreDebug) << "PdfSync: 'p*' line ignored";
2158 } else if (tokens.first() == QLatin1String("p") && tokenSize >= 4) {
2159 int id = tokens.at(1).toInt();
2161 if (it != points.end()) {
2162 it->x = tokens.at(2).toInt();
2163 it->y = tokens.at(3).toInt();
2164 it->page = currentpage;
2165 }
2166 } else if (line.startsWith(QLatin1Char('(')) && tokenSize == 1) {
2167 QString newfile = line;
2168 // chop the leading '('
2169 newfile.remove(0, 1);
2170 if (!newfile.endsWith(texStr)) {
2171 newfile += texStr;
2172 }
2173 fileStack.push(newfile);
2174 } else if (line == QLatin1String(")")) {
2175 if (!fileStack.isEmpty()) {
2176 fileStack.pop();
2177 } else {
2178 qCDebug(OkularCoreDebug) << "PdfSync: going one level down too much";
2179 }
2180 } else {
2181 qCDebug(OkularCoreDebug).nospace() << "PdfSync: unknown line format: '" << line << "'";
2182 }
2183 }
2184
2185 QVector<QList<Okular::SourceRefObjectRect *>> refRects(m_pagesVector.size());
2186 for (const pdfsyncpoint &pt : std::as_const(points)) {
2187 // drop pdfsync points not completely valid
2188 if (pt.page < 0 || pt.page >= m_pagesVector.size()) {
2189 continue;
2190 }
2191
2192 // magic numbers for TeX's RSU's (Ridiculously Small Units) conversion to pixels
2193 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()));
2194 QString file = pt.file;
2195 Okular::SourceReference *sourceRef = new Okular::SourceReference(file, pt.row, pt.column);
2196 refRects[pt.page].append(new Okular::SourceRefObjectRect(p, sourceRef));
2197 }
2198 for (int i = 0; i < refRects.size(); ++i) {
2199 if (!refRects.at(i).isEmpty()) {
2200 m_pagesVector[i]->setSourceReferences(refRects.at(i));
2201 }
2202 }
2203}
2204
2205void DocumentPrivate::clearAndWaitForRequests()
2206{
2207 m_pixmapRequestsMutex.lock();
2208 std::list<PixmapRequest *>::const_iterator sIt = m_pixmapRequestsStack.begin();
2209 std::list<PixmapRequest *>::const_iterator sEnd = m_pixmapRequestsStack.end();
2210 for (; sIt != sEnd; ++sIt) {
2211 delete *sIt;
2212 }
2213 m_pixmapRequestsStack.clear();
2214 m_pixmapRequestsMutex.unlock();
2215
2216 QEventLoop loop;
2217 bool startEventLoop = false;
2218 do {
2219 m_pixmapRequestsMutex.lock();
2220 startEventLoop = !m_executingPixmapRequests.empty();
2221
2222 if (m_generator->hasFeature(Generator::SupportsCancelling)) {
2223 for (PixmapRequest *executingRequest : std::as_const(m_executingPixmapRequests)) {
2224 executingRequest->d->mShouldAbortRender = 1;
2225 }
2226
2227 if (m_generator->d_ptr->mTextPageGenerationThread) {
2228 m_generator->d_ptr->mTextPageGenerationThread->abortExtraction();
2229 }
2230 }
2231
2232 m_pixmapRequestsMutex.unlock();
2233 if (startEventLoop) {
2234 m_closingLoop = &loop;
2235 loop.exec();
2236 m_closingLoop = nullptr;
2237 }
2238 } while (startEventLoop);
2239}
2240
2241int DocumentPrivate::findFieldPageNumber(Okular::FormField *field)
2242{
2243 // Lookup the page of the FormField
2244 int foundPage = -1;
2245 for (uint pageIdx = 0, nPages = m_parent->pages(); pageIdx < nPages; pageIdx++) {
2246 const Page *p = m_parent->page(pageIdx);
2247 if (p && p->formFields().contains(field)) {
2248 foundPage = static_cast<int>(pageIdx);
2249 break;
2250 }
2251 }
2252 return foundPage;
2253}
2254
2255void DocumentPrivate::executeScriptEvent(const std::shared_ptr<Event> &event, const Okular::ScriptAction *linkscript)
2256{
2257 if (!m_scripter) {
2258 m_scripter = new Scripter(this);
2259 }
2260 m_scripter->setEvent(event.get());
2261 m_scripter->execute(linkscript->scriptType(), linkscript->script());
2262
2263 // Clear out the event after execution
2264 m_scripter->setEvent(nullptr);
2265}
2266
2267Document::Document(QWidget *widget)
2268 : QObject(nullptr)
2269 , d(new DocumentPrivate(this))
2270{
2271 d->m_widget = widget;
2272 d->m_bookmarkManager = new BookmarkManager(d);
2273 d->m_viewportIterator = d->m_viewportHistory.insert(d->m_viewportHistory.end(), DocumentViewport());
2274 d->m_undoStack = new QUndoStack(this);
2275
2276 connect(SettingsCore::self(), &SettingsCore::configChanged, this, [this] { d->_o_configChanged(); });
2280
2281 qRegisterMetaType<Okular::FontInfo>();
2282}
2283
2285{
2286 // delete generator, pages, and related stuff
2287 closeDocument();
2288
2289 QSet<View *>::const_iterator viewIt = d->m_views.constBegin(), viewEnd = d->m_views.constEnd();
2290 for (; viewIt != viewEnd; ++viewIt) {
2291 View *v = *viewIt;
2292 v->d_func()->document = nullptr;
2293 }
2294
2295 // delete the bookmark manager
2296 delete d->m_bookmarkManager;
2297
2298 // delete the loaded generators
2299 QHash<QString, GeneratorInfo>::const_iterator it = d->m_loadedGenerators.constBegin(), itEnd = d->m_loadedGenerators.constEnd();
2300 for (; it != itEnd; ++it) {
2301 d->unloadGenerator(it.value());
2302 }
2303 d->m_loadedGenerators.clear();
2304
2305 // delete the private structure
2306 delete d;
2307}
2308
2309QString DocumentPrivate::docDataFileName(const QUrl &url, qint64 document_size)
2310{
2311 QString fn = url.fileName();
2312 fn = QString::number(document_size) + QLatin1Char('.') + fn + QStringLiteral(".xml");
2313 QString docdataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/okular/docdata");
2314 // make sure that the okular/docdata/ directory exists (probably this used to be handled by KStandardDirs)
2315 if (!QFileInfo::exists(docdataDir)) {
2316 qCDebug(OkularCoreDebug) << "creating docdata folder" << docdataDir;
2317 QDir().mkpath(docdataDir);
2318 }
2319 QString newokularfile = docdataDir + QLatin1Char('/') + fn;
2320
2321 return newokularfile;
2322}
2323
2324QVector<KPluginMetaData> DocumentPrivate::availableGenerators()
2325{
2326 static QVector<KPluginMetaData> result;
2327 if (result.isEmpty()) {
2328 result = KPluginMetaData::findPlugins(QStringLiteral("okular_generators"));
2329 }
2330 return result;
2331}
2332
2333KPluginMetaData DocumentPrivate::generatorForMimeType(const QMimeType &type, QWidget *widget, const QVector<KPluginMetaData> &triedOffers)
2334{
2335 // First try to find an exact match, and then look for more general ones (e. g. the plain text one)
2336 // Ideally we would rank these by "closeness", but that might be overdoing it
2337
2338 const QVector<KPluginMetaData> available = availableGenerators();
2340 QVector<KPluginMetaData> exactMatches;
2341
2342 QMimeDatabase mimeDatabase;
2343
2344 for (const KPluginMetaData &md : available) {
2345 if (triedOffers.contains(md)) {
2346 continue;
2347 }
2348
2349 const QStringList mimetypes = md.mimeTypes();
2350 for (const QString &supported : mimetypes) {
2351 QMimeType mimeType = mimeDatabase.mimeTypeForName(supported);
2352 if (mimeType == type && !exactMatches.contains(md)) {
2353 exactMatches << md;
2354 }
2355
2356 if (type.inherits(supported) && !offers.contains(md)) {
2357 offers << md;
2358 }
2359 }
2360 }
2361
2362 if (!exactMatches.isEmpty()) {
2363 offers = exactMatches;
2364 }
2365
2366 if (offers.isEmpty()) {
2367 return KPluginMetaData();
2368 }
2369 int hRank = 0;
2370 // best ranked offer search
2371 int offercount = offers.size();
2372 if (offercount > 1) {
2373 // sort the offers: the offers with an higher priority come before
2374 auto cmp = [](const KPluginMetaData &s1, const KPluginMetaData &s2) {
2375 const QString property = QStringLiteral("X-KDE-Priority");
2376 return s1.rawData().value(property).toInt() > s2.rawData().value(property).toInt();
2377 };
2378 std::stable_sort(offers.begin(), offers.end(), cmp);
2379
2380 if (SettingsCore::chooseGenerators()) {
2382 for (int i = 0; i < offercount; ++i) {
2383 list << offers.at(i).pluginId();
2384 }
2385 ChooseEngineDialog choose(list, type, widget);
2386
2387 if (choose.exec() == QDialog::Rejected) {
2388 return KPluginMetaData();
2389 }
2390
2391 hRank = choose.selectedGenerator();
2392 }
2393 }
2394 Q_ASSERT(hRank < offers.size());
2395 return offers.at(hRank);
2396}
2397
2398Document::OpenResult Document::openDocument(const QString &docFile, const QUrl &url, const QMimeType &_mime, const QString &password)
2399{
2400 QMimeDatabase db;
2401 QMimeType mime = _mime;
2402 QByteArray filedata;
2403 int fd = -1;
2404 if (url.scheme() == QLatin1String("fd")) {
2405 bool ok;
2406 fd = QStringView {url.path()}.mid(1).toInt(&ok);
2407 if (!ok) {
2408 return OpenError;
2409 }
2410 } else if (url.fileName() == QLatin1String("-")) {
2411 fd = 0;
2412 }
2413 bool triedMimeFromFileContent = false;
2414 if (fd < 0) {
2415 if (!mime.isValid()) {
2416 return OpenError;
2417 }
2418
2419 d->m_url = url;
2420 d->m_docFileName = docFile;
2421
2422 if (!d->updateMetadataXmlNameAndDocSize()) {
2423 return OpenError;
2424 }
2425 } else {
2426 QFile qstdin;
2427 const bool ret = qstdin.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle);
2428 if (!ret) {
2429 qWarning() << "failed to read" << url << filedata;
2430 return OpenError;
2431 }
2432
2433 filedata = qstdin.readAll();
2434 mime = db.mimeTypeForData(filedata);
2435 if (!mime.isValid() || mime.isDefault()) {
2436 return OpenError;
2437 }
2438 d->m_docSize = filedata.size();
2439 triedMimeFromFileContent = true;
2440 }
2441
2442 const bool fromFileDescriptor = fd >= 0;
2443
2444 // 0. load Generator
2445 // request only valid non-disabled plugins suitable for the mimetype
2446 KPluginMetaData offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2447 if (!offer.isValid() && !triedMimeFromFileContent) {
2449 triedMimeFromFileContent = true;
2450 if (newmime != mime) {
2451 mime = newmime;
2452 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2453 }
2454 if (!offer.isValid()) {
2455 // There's still no offers, do a final mime search based on the filename
2456 // We need this because sometimes (e.g. when downloading from a webserver) the mimetype we
2457 // use is the one fed by the server, that may be wrong
2458 newmime = db.mimeTypeForUrl(url);
2459
2460 if (!newmime.isDefault() && newmime != mime) {
2461 mime = newmime;
2462 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2463 }
2464 }
2465 }
2466 if (!offer.isValid()) {
2467 d->m_openError = i18n("Can not find a plugin which is able to handle the document being passed.");
2468 Q_EMIT error(d->m_openError, -1);
2469 qCWarning(OkularCoreDebug).nospace() << "No plugin for mimetype '" << mime.name() << "'.";
2470 return OpenError;
2471 }
2472
2473 // 1. load Document
2474 OpenResult openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2475 if (openResult == OpenError) {
2476 QVector<KPluginMetaData> triedOffers;
2477 triedOffers << offer;
2478 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2479
2480 while (offer.isValid()) {
2481 openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2482
2483 if (openResult == OpenError) {
2484 triedOffers << offer;
2485 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2486 } else {
2487 break;
2488 }
2489 }
2490
2491 if (openResult == OpenError && !triedMimeFromFileContent) {
2493 triedMimeFromFileContent = true;
2494 if (newmime != mime) {
2495 mime = newmime;
2496 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2497 while (offer.isValid()) {
2498 openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2499
2500 if (openResult == OpenError) {
2501 triedOffers << offer;
2502 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2503 } else {
2504 break;
2505 }
2506 }
2507 }
2508 }
2509
2510 if (openResult == OpenSuccess) {
2511 // Clear errors, since we're trying various generators, maybe one of them errored out
2512 // but we finally succeeded
2513 // TODO one can still see the error message animating out but since this is a very rare
2514 // condition we can leave this for future work
2515 Q_EMIT error(QString(), -1);
2516 }
2517 }
2518 if (openResult != OpenSuccess) {
2519 return openResult;
2520 }
2521
2522 // no need to check for the existence of a synctex file, no parser will be
2523 // created if none exists
2524 d->m_synctex_scanner = synctex_scanner_new_with_output_file(QFile::encodeName(docFile).constData(), nullptr, 1);
2525 if (!d->m_synctex_scanner && QFile::exists(docFile + QLatin1String("sync"))) {
2526 d->loadSyncFile(docFile);
2527 }
2528
2529 d->m_generatorName = offer.pluginId();
2530 d->m_pageController = new PageController();
2531 connect(d->m_pageController, &PageController::rotationFinished, this, [this](int p, Okular::Page *op) { d->rotationFinished(p, op); });
2532
2533 for (Page *p : std::as_const(d->m_pagesVector)) {
2534 p->d->m_doc = d;
2535 }
2536
2537 d->m_docdataMigrationNeeded = false;
2538
2539 // 2. load Additional Data (bookmarks, local annotations and metadata) about the document
2540 if (d->m_archiveData) {
2541 // QTemporaryFile is weird and will return false in exists if fileName wasn't called before
2542 d->m_archiveData->metadataFile.fileName();
2543 d->loadDocumentInfo(d->m_archiveData->metadataFile, LoadPageInfo);
2544 d->loadDocumentInfo(LoadGeneralInfo);
2545 } else {
2546 if (d->loadDocumentInfo(LoadPageInfo)) {
2547 d->m_docdataMigrationNeeded = true;
2548 }
2549 d->loadDocumentInfo(LoadGeneralInfo);
2550 }
2551
2552 d->m_bookmarkManager->setUrl(d->m_url);
2553
2554 // 3. setup observers internal lists and data
2555 foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged));
2556
2557 // 4. set initial page (restoring the page saved in xml if loaded)
2558 DocumentViewport loadedViewport = (*d->m_viewportIterator);
2559 if (loadedViewport.isValid()) {
2560 (*d->m_viewportIterator) = DocumentViewport();
2561 if (loadedViewport.pageNumber >= (int)d->m_pagesVector.size()) {
2562 loadedViewport.pageNumber = d->m_pagesVector.size() - 1;
2563 }
2564 } else {
2565 loadedViewport.pageNumber = 0;
2566 }
2567 setViewport(loadedViewport);
2568
2569 // start bookmark saver timer
2570 if (!d->m_saveBookmarksTimer) {
2571 d->m_saveBookmarksTimer = new QTimer(this);
2572 connect(d->m_saveBookmarksTimer, &QTimer::timeout, this, [this] { d->saveDocumentInfo(); });
2573 }
2574 d->m_saveBookmarksTimer->start(5 * 60 * 1000);
2575
2576 // start memory check timer
2577 if (!d->m_memCheckTimer) {
2578 d->m_memCheckTimer = new QTimer(this);
2579 connect(d->m_memCheckTimer, &QTimer::timeout, this, [this] { d->slotTimedMemoryCheck(); });
2580 }
2581 d->m_memCheckTimer->start(kMemCheckTime);
2582
2583 const DocumentViewport nextViewport = d->nextDocumentViewport();
2584 if (nextViewport.isValid()) {
2585 setViewport(nextViewport);
2586 d->m_nextDocumentViewport = DocumentViewport();
2587 d->m_nextDocumentDestination = QString();
2588 }
2589
2590 AudioPlayer::instance()->setDocument(fromFileDescriptor ? QUrl() : d->m_url, this);
2591
2592 const QStringList docScripts = d->m_generator->metaData(QStringLiteral("DocumentScripts"), QStringLiteral("JavaScript")).toStringList();
2593 if (!docScripts.isEmpty()) {
2594 d->m_scripter = new Scripter(d);
2595 for (const QString &docscript : docScripts) {
2596 const Okular::ScriptAction *linkScript = new Okular::ScriptAction(Okular::JavaScript, docscript);
2597 std::shared_ptr<Event> event = Event::createDocEvent(Event::DocOpen);
2598 d->executeScriptEvent(event, linkScript);
2599 }
2600 }
2601
2602 return OpenSuccess;
2603}
2604
2605bool DocumentPrivate::updateMetadataXmlNameAndDocSize()
2606{
2607 // m_docFileName is always local so we can use QFileInfo on it
2608 QFileInfo fileReadTest(m_docFileName);
2609 if (!fileReadTest.isFile() && !fileReadTest.isReadable()) {
2610 return false;
2611 }
2612
2613 m_docSize = fileReadTest.size();
2614
2615 // determine the related "xml document-info" filename
2616 if (m_url.isLocalFile()) {
2617 const QString filePath = docDataFileName(m_url, m_docSize);
2618 qCDebug(OkularCoreDebug) << "Metadata file is now:" << filePath;
2619 m_xmlFileName = filePath;
2620 } else {
2621 qCDebug(OkularCoreDebug) << "Metadata file: disabled";
2622 m_xmlFileName = QString();
2623 }
2624
2625 return true;
2626}
2627
2629{
2630 if (d->m_generator) {
2632 if (iface) {
2633 return iface->guiClient();
2634 }
2635 }
2636 return nullptr;
2637}
2638
2640{
2641 // check if there's anything to close...
2642 if (!d->m_generator) {
2643 return;
2644 }
2645
2646 if (const Okular::Action *action = d->m_generator->additionalDocumentAction(CloseDocument)) {
2647 processDocumentAction(action, CloseDocument);
2648 }
2649
2651
2652 delete d->m_pageController;
2653 d->m_pageController = nullptr;
2654
2655 delete d->m_scripter;
2656 d->m_scripter = nullptr;
2657
2658 // remove requests left in queue
2659 d->clearAndWaitForRequests();
2660
2661 if (d->m_fontThread) {
2662 disconnect(d->m_fontThread, nullptr, this, nullptr);
2663 d->m_fontThread->stopExtraction();
2664 d->m_fontThread->wait();
2665 d->m_fontThread = nullptr;
2666 }
2667
2668 // stop any audio playback
2670
2671 // close the current document and save document info if a document is still opened
2672 if (d->m_generator && d->m_pagesVector.size() > 0) {
2673 d->saveDocumentInfo();
2674
2675 // free the content of the opaque backend actions (if any)
2676 // this is a bit awkward since backends can store "random stuff" in the
2677 // BackendOpaqueAction nativeId qvariant so we need to tell them to free it
2678 // ideally we would just do that in the BackendOpaqueAction destructor
2679 // but that's too late in the cleanup process, i.e. the generator has already closed its document
2680 // and the document generator is nullptr
2681 for (const Page *p : std::as_const(d->m_pagesVector)) {
2682 const QList<ObjectRect *> &oRects = p->objectRects();
2683 for (const ObjectRect *oRect : oRects) {
2684 if (oRect->objectType() == ObjectRect::Action) {
2685 const Action *a = static_cast<const Action *>(oRect->object());
2686 const BackendOpaqueAction *backendAction = dynamic_cast<const BackendOpaqueAction *>(a);
2687 if (backendAction) {
2688 d->m_generator->freeOpaqueActionContents(*backendAction);
2689 }
2690 }
2691 }
2692
2693 const QList<FormField *> forms = p->formFields();
2694 for (const FormField *form : forms) {
2695 const QList<Action *> additionalActions = form->additionalActions();
2696 for (const Action *a : additionalActions) {
2697 const BackendOpaqueAction *backendAction = dynamic_cast<const BackendOpaqueAction *>(a);
2698 if (backendAction) {
2699 d->m_generator->freeOpaqueActionContents(*backendAction);
2700 }
2701 }
2702 }
2703 }
2704
2705 d->m_generator->closeDocument();
2706 }
2707
2708 if (d->m_synctex_scanner) {
2709 synctex_scanner_free(d->m_synctex_scanner);
2710 d->m_synctex_scanner = nullptr;
2711 }
2712
2713 // stop timers
2714 if (d->m_memCheckTimer) {
2715 d->m_memCheckTimer->stop();
2716 }
2717 if (d->m_saveBookmarksTimer) {
2718 d->m_saveBookmarksTimer->stop();
2719 }
2720
2721 if (d->m_generator) {
2722 // disconnect the generator from this document ...
2723 d->m_generator->d_func()->m_document = nullptr;
2724 // .. and this document from the generator signals
2725 disconnect(d->m_generator, nullptr, this, nullptr);
2726
2727 QHash<QString, GeneratorInfo>::const_iterator genIt = d->m_loadedGenerators.constFind(d->m_generatorName);
2728 Q_ASSERT(genIt != d->m_loadedGenerators.constEnd());
2729 }
2730 d->m_generator = nullptr;
2731 d->m_generatorName = QString();
2732 d->m_url = QUrl();
2733 d->m_walletGenerator = nullptr;
2734 d->m_docFileName = QString();
2735 d->m_xmlFileName = QString();
2736 delete d->m_tempFile;
2737 d->m_tempFile = nullptr;
2738 delete d->m_archiveData;
2739 d->m_archiveData = nullptr;
2740 d->m_docSize = -1;
2741 d->m_exportCached = false;
2742 d->m_exportFormats.clear();
2743 d->m_exportToText = ExportFormat();
2744 d->m_fontsCached = false;
2745 d->m_fontsCache.clear();
2746 d->m_rotation = Rotation0;
2747
2748 // send an empty list to observers (to free their data)
2750
2751 // delete pages and clear 'd->m_pagesVector' container
2752 QVector<Page *>::const_iterator pIt = d->m_pagesVector.constBegin();
2753 QVector<Page *>::const_iterator pEnd = d->m_pagesVector.constEnd();
2754 for (; pIt != pEnd; ++pIt) {
2755 delete *pIt;
2756 }
2757 d->m_pagesVector.clear();
2758
2759 // clear 'memory allocation' descriptors
2760 qDeleteAll(d->m_allocatedPixmaps);
2761 d->m_allocatedPixmaps.clear();
2762
2763 // clear 'running searches' descriptors
2764 QMap<int, RunningSearch *>::const_iterator rIt = d->m_searches.constBegin();
2765 QMap<int, RunningSearch *>::const_iterator rEnd = d->m_searches.constEnd();
2766 for (; rIt != rEnd; ++rIt) {
2767 delete *rIt;
2768 }
2769 d->m_searches.clear();
2770
2771 // clear the visible areas and notify the observers
2772 QVector<VisiblePageRect *>::const_iterator vIt = d->m_pageRects.constBegin();
2773 QVector<VisiblePageRect *>::const_iterator vEnd = d->m_pageRects.constEnd();
2774 for (; vIt != vEnd; ++vIt) {
2775 delete *vIt;
2776 }
2777 d->m_pageRects.clear();
2778 foreachObserver(notifyVisibleRectsChanged());
2779
2780 // reset internal variables
2781
2782 d->m_viewportHistory.clear();
2783 d->m_viewportHistory.emplace_back();
2784 d->m_viewportIterator = d->m_viewportHistory.begin();
2785 d->m_allocatedPixmapsTotalMemory = 0;
2786 d->m_allocatedTextPagesFifo.clear();
2787 d->m_pageSize = PageSize();
2788 d->m_pageSizes.clear();
2789
2790 d->m_documentInfo = DocumentInfo();
2791 d->m_documentInfoAskedKeys.clear();
2792
2793 AudioPlayer::instance()->resetDocument();
2794
2795 d->m_undoStack->clear();
2796 d->m_docdataMigrationNeeded = false;
2797
2798#if HAVE_MALLOC_TRIM
2799 // trim unused memory, glibc should do this but it seems it does not
2800 // this can greatly decrease the [perceived] memory consumption of okular
2801 // see: https://sourceware.org/bugzilla/show_bug.cgi?id=14827
2802 malloc_trim(0);
2803#endif
2804}
2805
2807{
2808 Q_ASSERT(!d->m_observers.contains(pObserver));
2809 d->m_observers << pObserver;
2810
2811 // if the observer is added while a document is already opened, tell it
2812 if (!d->m_pagesVector.isEmpty()) {
2814 pObserver->notifyViewportChanged(false /*disables smoothMove*/);
2815 }
2816}
2817
2819{
2820 // remove observer from the set. it won't receive notifications anymore
2821 if (d->m_observers.contains(pObserver)) {
2822 // free observer's pixmap data
2823 QVector<Page *>::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd();
2824 for (; it != end; ++it) {
2825 (*it)->deletePixmap(pObserver);
2826 }
2827
2828 // [MEM] free observer's allocation descriptors
2829 std::list<AllocatedPixmap *>::iterator aIt = d->m_allocatedPixmaps.begin();
2830 std::list<AllocatedPixmap *>::iterator aEnd = d->m_allocatedPixmaps.end();
2831 while (aIt != aEnd) {
2832 AllocatedPixmap *p = *aIt;
2833 if (p->observer == pObserver) {
2834 aIt = d->m_allocatedPixmaps.erase(aIt);
2835 delete p;
2836 } else {
2837 ++aIt;
2838 }
2839 }
2840
2841 for (PixmapRequest *executingRequest : std::as_const(d->m_executingPixmapRequests)) {
2842 if (executingRequest->observer() == pObserver) {
2843 d->cancelRenderingBecauseOf(executingRequest, nullptr);
2844 }
2845 }
2846
2847 // remove observer entry from the set
2848 d->m_observers.remove(pObserver);
2849 }
2850}
2851
2853{
2854 // reparse generator config and if something changed clear Pages
2855 bool configchanged = false;
2856 if (d->m_generator) {
2858 if (iface) {
2859 configchanged = iface->reparseConfig();
2860 }
2861 }
2862 if (configchanged) {
2863 // invalidate pixmaps
2864 QVector<Page *>::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd();
2865 for (; it != end; ++it) {
2866 (*it)->deletePixmaps();
2867 }
2868
2869 // [MEM] remove allocation descriptors
2870 qDeleteAll(d->m_allocatedPixmaps);
2871 d->m_allocatedPixmaps.clear();
2872 d->m_allocatedPixmapsTotalMemory = 0;
2873
2874 // send reload signals to observers
2875 foreachObserver(notifyContentsCleared(DocumentObserver::Pixmap));
2876 }
2877
2878 // free memory if in 'low' profile
2879 if (SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !d->m_allocatedPixmaps.empty() && !d->m_pagesVector.isEmpty()) {
2880 d->cleanupPixmapMemory();
2881 }
2882}
2883
2885{
2886 return d->m_generator;
2887}
2888
2890{
2891 if (d->m_generator) {
2893 return iface ? true : false;
2894 } else {
2895 return false;
2896 }
2897}
2898
2899bool Document::sign(const NewSignatureData &data, const QString &newPath)
2900{
2901 if (d->m_generator->canSign()) {
2902 return d->m_generator->sign(data, newPath);
2903 } else {
2904 return false;
2905 }
2906}
2907
2909{
2910 return d->m_generator ? d->m_generator->certificateStore() : nullptr;
2911}
2912
2914{
2915 d->editorCommandOverride = editCmd;
2916}
2917
2919{
2920 return d->editorCommandOverride;
2921}
2922
2924{
2927 keys << ks;
2928 }
2929
2930 return documentInfo(keys);
2931}
2932
2934{
2935 DocumentInfo result = d->m_documentInfo;
2936 const QSet<DocumentInfo::Key> missingKeys = keys - d->m_documentInfoAskedKeys;
2937
2938 if (d->m_generator && !missingKeys.isEmpty()) {
2939 DocumentInfo info = d->m_generator->generateDocumentInfo(missingKeys);
2940
2941 if (missingKeys.contains(DocumentInfo::FilePath)) {
2942 info.set(DocumentInfo::FilePath, currentDocument().toDisplayString());
2943 }
2944
2945 if (d->m_docSize != -1 && missingKeys.contains(DocumentInfo::DocumentSize)) {
2946 const QString sizeString = KFormat().formatByteSize(d->m_docSize);
2947 info.set(DocumentInfo::DocumentSize, sizeString);
2948 }
2949 if (missingKeys.contains(DocumentInfo::PagesSize)) {
2950 const QString pagesSize = d->pagesSizeString();
2951 if (!pagesSize.isEmpty()) {
2952 info.set(DocumentInfo::PagesSize, pagesSize);
2953 }
2954 }
2955
2956 if (missingKeys.contains(DocumentInfo::Pages) && info.get(DocumentInfo::Pages).isEmpty()) {
2958 }
2959
2960 d->m_documentInfo.d->values.insert(info.d->values);
2961 d->m_documentInfo.d->titles.insert(info.d->titles);
2962 result.d->values.insert(info.d->values);
2963 result.d->titles.insert(info.d->titles);
2964 }
2965 d->m_documentInfoAskedKeys += keys;
2966
2967 return result;
2968}
2969
2971{
2972 return d->m_generator ? d->m_generator->generateDocumentSynopsis() : nullptr;
2973}
2974
2976{
2977 if (!d->m_generator || !d->m_generator->hasFeature(Generator::FontInfo) || d->m_fontThread) {
2978 return;
2979 }
2980
2981 if (d->m_fontsCached) {
2982 // in case we have cached fonts, simulate a reading
2983 // this way the API is the same, and users no need to care about the
2984 // internal caching
2985 for (int i = 0; i < d->m_fontsCache.count(); ++i) {
2986 Q_EMIT gotFont(d->m_fontsCache.at(i));
2988 }
2990 return;
2991 }
2992
2993 d->m_fontThread = new FontExtractionThread(d->m_generator, pages());
2994 connect(d->m_fontThread, &FontExtractionThread::gotFont, this, [this](const Okular::FontInfo &f) { d->fontReadingGotFont(f); });
2995 connect(d->m_fontThread.data(), &FontExtractionThread::progress, this, [this](int p) { d->slotFontReadingProgress(p); });
2996
2997 d->m_fontThread->startExtraction(/*d->m_generator->hasFeature( Generator::Threaded )*/ true);
2998}
2999
3001{
3002 if (!d->m_fontThread) {
3003 return;
3004 }
3005
3006 disconnect(d->m_fontThread, nullptr, this, nullptr);
3007 d->m_fontThread->stopExtraction();
3008 d->m_fontThread = nullptr;
3009 d->m_fontsCache.clear();
3010}
3011
3013{
3014 return d->m_generator ? d->m_generator->hasFeature(Generator::FontInfo) : false;
3015}
3016
3018{
3019 return d->m_generator ? d->m_generator->canSign() : false;
3020}
3021
3023{
3024 return d->m_generator ? d->m_generator->embeddedFiles() : nullptr;
3025}
3026
3027const Page *Document::page(int n) const
3028{
3029 return (n >= 0 && n < d->m_pagesVector.count()) ? d->m_pagesVector.at(n) : nullptr;
3030}
3031
3033{
3034 return (*d->m_viewportIterator);
3035}
3036
3038{
3039 return d->m_pageRects;
3040}
3041
3043{
3044 QVector<VisiblePageRect *>::const_iterator vIt = d->m_pageRects.constBegin();
3045 QVector<VisiblePageRect *>::const_iterator vEnd = d->m_pageRects.constEnd();
3046 for (; vIt != vEnd; ++vIt) {
3047 delete *vIt;
3048 }
3049 d->m_pageRects = visiblePageRects;
3050 // notify change to all other (different from id) observers
3051 for (DocumentObserver *o : std::as_const(d->m_observers)) {
3052 if (o != excludeObserver) {
3053 o->notifyVisibleRectsChanged();
3054 }
3055 }
3056}
3057
3059{
3060 return (*d->m_viewportIterator).pageNumber;
3061}
3062
3064{
3065 return d->m_pagesVector.size();
3066}
3067
3069{
3070 return d->m_url;
3071}
3072
3074{
3075 if (action == Okular::AllowNotes && (d->m_docdataMigrationNeeded || !d->m_annotationEditingEnabled)) {
3076 return false;
3077 }
3078 if (action == Okular::AllowFillForms && d->m_docdataMigrationNeeded) {
3079 return false;
3080 }
3081
3082#if !OKULAR_FORCE_DRM
3083 if (KAuthorized::authorize(QStringLiteral("skip_drm")) && !SettingsCore::obeyDRM()) {
3084 return true;
3085 }
3086#endif
3087
3088 return d->m_generator ? d->m_generator->isAllowed(action) : false;
3089}
3090
3092{
3093 return d->m_generator ? d->m_generator->hasFeature(Generator::TextExtraction) : false;
3094}
3095
3097{
3098 return d->m_generator ? d->m_generator->hasFeature(Generator::PageSizes) : false;
3099}
3100
3102{
3103 return d->m_generator ? d->m_generator->hasFeature(Generator::TiledRendering) : false;
3104}
3105
3107{
3108 if (d->m_generator) {
3109 if (d->m_pageSizes.isEmpty()) {
3110 d->m_pageSizes = d->m_generator->pageSizes();
3111 }
3112 return d->m_pageSizes;
3113 }
3114 return PageSize::List();
3115}
3116
3118{
3119 if (!d->m_generator) {
3120 return false;
3121 }
3122
3123 d->cacheExportFormats();
3124 return !d->m_exportToText.isNull();
3125}
3126
3127bool Document::exportToText(const QString &fileName) const
3128{
3129 if (!d->m_generator) {
3130 return false;
3131 }
3132
3133 d->cacheExportFormats();
3134 if (d->m_exportToText.isNull()) {
3135 return false;
3136 }
3137
3138 return d->m_generator->exportTo(fileName, d->m_exportToText);
3139}
3140
3142{
3143 if (!d->m_generator) {
3144 return ExportFormat::List();
3145 }
3146
3147 d->cacheExportFormats();
3148 return d->m_exportFormats;
3149}
3150
3151bool Document::exportTo(const QString &fileName, const ExportFormat &format) const
3152{
3153 return d->m_generator ? d->m_generator->exportTo(fileName, format) : false;
3154}
3155
3157{
3158 return d->m_viewportIterator == d->m_viewportHistory.begin();
3159}
3160
3162{
3163 return d->m_viewportIterator == --(d->m_viewportHistory.end());
3164}
3165
3166QVariant Document::metaData(const QString &key, const QVariant &option) const
3167{
3168 // if option starts with "src:" assume that we are handling a
3169 // source reference
3170 if (key == QLatin1String("NamedViewport") && option.toString().startsWith(QLatin1String("src:"), Qt::CaseInsensitive) && d->m_synctex_scanner) {
3171 const QString reference = option.toString();
3172
3173 // The reference is of form "src:1111Filename", where "1111"
3174 // points to line number 1111 in the file "Filename".
3175 // Extract the file name and the numeral part from the reference string.
3176 // This will fail if Filename starts with a digit.
3177 QString name, lineString;
3178 // Remove "src:". Presence of substring has been checked before this
3179 // function is called.
3180 name = reference.mid(4);
3181 // split
3182 int nameLength = name.length();
3183 int i = 0;
3184 for (i = 0; i < nameLength; ++i) {
3185 if (!name[i].isDigit()) {
3186 break;
3187 }
3188 }
3189 lineString = name.left(i);
3190 name = name.mid(i);
3191 // Remove spaces.
3192 name = name.trimmed();
3193 lineString = lineString.trimmed();
3194 // Convert line to integer.
3195 bool ok;
3196 int line = lineString.toInt(&ok);
3197 if (!ok) {
3198 line = -1;
3199 }
3200
3201 // Use column == -1 for now.
3202 if (synctex_display_query(d->m_synctex_scanner, QFile::encodeName(name).constData(), line, -1, 0) > 0) {
3203 synctex_node_p node;
3204 // For now use the first hit. Could possibly be made smarter
3205 // in case there are multiple hits.
3206 while ((node = synctex_scanner_next_result(d->m_synctex_scanner))) {
3208
3209 // TeX pages start at 1.
3210 viewport.pageNumber = synctex_node_page(node) - 1;
3211
3212 if (viewport.pageNumber >= 0) {
3213 const QSizeF dpi = d->m_generator->dpi();
3214
3215 // TeX small points ...
3216 double px = (synctex_node_visible_h(node) * dpi.width()) / 72.27;
3217 double py = (synctex_node_visible_v(node) * dpi.height()) / 72.27;
3218 viewport.rePos.normalizedX = px / page(viewport.pageNumber)->width();
3219 viewport.rePos.normalizedY = (py + 0.5) / page(viewport.pageNumber)->height();
3220 viewport.rePos.enabled = true;
3222
3223 return viewport.toString();
3224 }
3225 }
3226 }
3227 }
3228 return d->m_generator ? d->m_generator->metaData(key, option) : QVariant();
3229}
3230
3232{
3233 return d->m_rotation;
3234}
3235
3237{
3238 bool allPagesSameSize = true;
3239 QSizeF size;
3240 for (int i = 0; allPagesSameSize && i < d->m_pagesVector.count(); ++i) {
3241 const Page *p = d->m_pagesVector.at(i);
3242 if (i == 0) {
3243 size = QSizeF(p->width(), p->height());
3244 } else {
3245 allPagesSameSize = (size == QSizeF(p->width(), p->height()));
3246 }
3247 }
3248 if (allPagesSameSize) {
3249 return size;
3250 } else {
3251 return QSizeF();
3252 }
3253}
3254
3256{
3257 if (d->m_generator) {
3258 if (d->m_generator->pagesSizeMetric() != Generator::None) {
3259 const Page *p = d->m_pagesVector.at(page);
3260 return d->localizedSize(QSizeF(p->width(), p->height()));
3261 }
3262 }
3263 return QString();
3264}
3265
3266static bool shouldCancelRenderingBecauseOf(const PixmapRequest &executingRequest, const PixmapRequest &otherRequest)
3267{
3268 // New request has higher priority -> cancel
3269 if (executingRequest.priority() > otherRequest.priority()) {
3270 return true;
3271 }
3272
3273 // New request has lower priority -> don't cancel
3274 if (executingRequest.priority() < otherRequest.priority()) {
3275 return false;
3276 }
3277
3278 // New request has same priority and is from a different observer -> don't cancel
3279 // AFAIK this never happens since all observers have different priorities
3280 if (executingRequest.observer() != otherRequest.observer()) {
3281 return false;
3282 }
3283
3284 // Same priority and observer, different page number -> don't cancel
3285 // may still end up cancelled later in the parent caller if none of the requests
3286 // is of the executingRequest page and RemoveAllPrevious is specified
3287 if (executingRequest.pageNumber() != otherRequest.pageNumber()) {
3288 return false;
3289 }
3290
3291 // Same priority, observer, page, different size -> cancel
3292 if (executingRequest.width() != otherRequest.width()) {
3293 return true;
3294 }
3295
3296 // Same priority, observer, page, different size -> cancel
3297 if (executingRequest.height() != otherRequest.height()) {
3298 return true;
3299 }
3300
3301 // Same priority, observer, page, different tiling -> cancel
3302 if (executingRequest.isTile() != otherRequest.isTile()) {
3303 return true;
3304 }
3305
3306 // Same priority, observer, page, different tiling -> cancel
3307 if (executingRequest.isTile()) {
3308 const NormalizedRect bothRequestsRect = executingRequest.normalizedRect() | otherRequest.normalizedRect();
3309 if (!(bothRequestsRect == executingRequest.normalizedRect())) {
3310 return true;
3311 }
3312 }
3313
3314 return false;
3315}
3316
3317bool DocumentPrivate::cancelRenderingBecauseOf(PixmapRequest *executingRequest, PixmapRequest *newRequest)
3318{
3319 // No point in aborting the rendering already finished, let it go through
3320 if (!executingRequest->d->mResultImage.isNull()) {
3321 return false;
3322 }
3323
3324 if (newRequest && newRequest->asynchronous() && executingRequest->partialUpdatesWanted()) {
3325 newRequest->setPartialUpdatesWanted(true);
3326 }
3327
3328 TilesManager *tm = executingRequest->d->tilesManager();
3329 if (tm) {
3330 tm->setPixmap(nullptr, executingRequest->normalizedRect(), true /*isPartialPixmap*/);
3331 tm->setRequest(NormalizedRect(), 0, 0);
3332 }
3333 PagePrivate::PixmapObject object = executingRequest->page()->d->m_pixmaps.take(executingRequest->observer());
3334 delete object.m_pixmap;
3335
3336 if (executingRequest->d->mShouldAbortRender != 0) {
3337 return false;
3338 }
3339
3340 executingRequest->d->mShouldAbortRender = 1;
3341
3342 if (m_generator->d_ptr->mTextPageGenerationThread && m_generator->d_ptr->mTextPageGenerationThread->page() == executingRequest->page()) {
3343 m_generator->d_ptr->mTextPageGenerationThread->abortExtraction();
3344 }
3345
3346 return true;
3347}
3348
3350{
3352}
3353
3355{
3356 if (requests.isEmpty()) {
3357 return;
3358 }
3359
3360 if (!d->m_pageController) {
3361 // delete requests..
3362 qDeleteAll(requests);
3363 // ..and return
3364 return;
3365 }
3366
3367 QSet<DocumentObserver *> observersPixmapCleared;
3368
3369 // 1. [CLEAN STACK] remove previous requests of requesterID
3370 const DocumentObserver *requesterObserver = requests.first()->observer();
3371 QSet<int> requestedPages;
3372 {
3373 for (const PixmapRequest *request : requests) {
3374 Q_ASSERT(request->observer() == requesterObserver);
3375 requestedPages.insert(request->pageNumber());
3376 }
3377 }
3378 const bool removeAllPrevious = reqOptions & RemoveAllPrevious;
3379 d->m_pixmapRequestsMutex.lock();
3380 std::list<PixmapRequest *>::iterator sIt = d->m_pixmapRequestsStack.begin(), sEnd = d->m_pixmapRequestsStack.end();
3381 while (sIt != sEnd) {
3382 if ((*sIt)->observer() == requesterObserver && (removeAllPrevious || requestedPages.contains((*sIt)->pageNumber()))) {
3383 // delete request and remove it from stack
3384 delete *sIt;
3385 sIt = d->m_pixmapRequestsStack.erase(sIt);
3386 } else {
3387 ++sIt;
3388 }
3389 }
3390
3391 // 1.B [PREPROCESS REQUESTS] tweak some values of the requests
3392 for (PixmapRequest *request : requests) {
3393 // set the 'page field' (see PixmapRequest) and check if it is valid
3394 qCDebug(OkularCoreDebug).nospace() << "request observer=" << request->observer() << " " << request->width() << "x" << request->height() << "@" << request->pageNumber();
3395 if (d->m_pagesVector.value(request->pageNumber()) == nullptr) {
3396 // skip requests referencing an invalid page (must not happen)
3397 delete request;
3398 continue;
3399 }
3400
3401 request->d->mPage = d->m_pagesVector.value(request->pageNumber());
3402
3403 if (request->isTile()) {
3404 // Change the current request rect so that only invalid tiles are
3405 // requested. Also make sure the rect is tile-aligned.
3406 NormalizedRect tilesRect;
3407 const QList<Tile> tiles = request->d->tilesManager()->tilesAt(request->normalizedRect(), TilesManager::TerminalTile);
3408 QList<Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
3409 while (tIt != tEnd) {
3410 const Tile &tile = *tIt;
3411 if (!tile.isValid()) {
3412 if (tilesRect.isNull()) {
3413 tilesRect = tile.rect();
3414 } else {
3415 tilesRect |= tile.rect();
3416 }
3417 }
3418
3419 tIt++;
3420 }
3421
3422 request->setNormalizedRect(tilesRect);
3423 }
3424
3425 if (!request->asynchronous()) {
3426 request->d->mPriority = 0;
3427 }
3428 }
3429
3430 // 1.C [CANCEL REQUESTS] cancel those requests that are running and should be cancelled because of the new requests coming in
3431 if (d->m_generator->hasFeature(Generator::SupportsCancelling)) {
3432 for (PixmapRequest *executingRequest : std::as_const(d->m_executingPixmapRequests)) {
3433 bool newRequestsContainExecutingRequestPage = false;
3434 bool requestCancelled = false;
3435 for (PixmapRequest *newRequest : requests) {
3436 if (newRequest->pageNumber() == executingRequest->pageNumber() && requesterObserver == executingRequest->observer()) {
3437 newRequestsContainExecutingRequestPage = true;
3438 }
3439
3440 if (shouldCancelRenderingBecauseOf(*executingRequest, *newRequest)) {
3441 requestCancelled = d->cancelRenderingBecauseOf(executingRequest, newRequest);
3442 }
3443 }
3444
3445 // If we were told to remove all the previous requests and the executing request page is not part of the new requests, cancel it
3446 if (!requestCancelled && removeAllPrevious && requesterObserver == executingRequest->observer() && !newRequestsContainExecutingRequestPage) {
3447 requestCancelled = d->cancelRenderingBecauseOf(executingRequest, nullptr);
3448 }
3449
3450 if (requestCancelled) {
3451 observersPixmapCleared << executingRequest->observer();
3452 }
3453 }
3454 }
3455
3456 // 2. [ADD TO STACK] add requests to stack
3457 for (PixmapRequest *request : requests) {
3458 // add request to the 'stack' at the right place
3459 if (!request->priority()) {
3460 // add priority zero requests to the top of the stack
3461 d->m_pixmapRequestsStack.push_back(request);
3462 } else {
3463 // insert in stack sorted by priority
3464 sIt = d->m_pixmapRequestsStack.begin();
3465 sEnd = d->m_pixmapRequestsStack.end();
3466 while (sIt != sEnd && (*sIt)->priority() > request->priority()) {
3467 ++sIt;
3468 }
3469 d->m_pixmapRequestsStack.insert(sIt, request);
3470 }
3471 }
3472 d->m_pixmapRequestsMutex.unlock();
3473
3474 // 3. [START FIRST GENERATION] if <NO>generator is ready, start a new generation,
3475 // or else (if gen is running) it will be started when the new contents will
3476 // come from generator (in requestDone())</NO>
3477 // all handling of requests put into sendGeneratorPixmapRequest
3478 // if ( generator->canRequestPixmap() )
3479 d->sendGeneratorPixmapRequest();
3480
3481 for (DocumentObserver *o : std::as_const(observersPixmapCleared)) {
3482 o->notifyContentsCleared(Okular::DocumentObserver::Pixmap);
3483 }
3484}
3485
3486void Document::requestTextPage(uint pageNumber)
3487{
3488 Page *kp = d->m_pagesVector[pageNumber];
3489 if (!d->m_generator || !kp) {
3490 return;
3491 }
3492
3493 // Memory management for TextPages
3494
3495 d->m_generator->generateTextPage(kp);
3496}
3497
3498void DocumentPrivate::notifyAnnotationChanges(int page)
3499{
3500 foreachObserverD(notifyPageChanged(page, DocumentObserver::Annotations));
3501}
3502
3503void DocumentPrivate::notifyFormChanges(int /*page*/)
3504{
3505 recalculateForms();
3506}
3507
3509{
3510 d->recalculateForms();
3511}
3512
3513void Document::addPageAnnotation(int page, Annotation *annotation)
3514{
3515 // Transform annotation's base boundary rectangle into unrotated coordinates
3516 Page *p = d->m_pagesVector[page];
3517 QTransform t = p->d->rotationMatrix();
3518 annotation->d_ptr->baseTransform(t.inverted());
3519 QUndoCommand *uc = new AddAnnotationCommand(this->d, annotation, page);
3520 d->m_undoStack->push(uc);
3521}
3522
3524{
3525 if (!annotation || (annotation->flags() & Annotation::DenyWrite)) {
3526 return false;
3527 }
3528
3530 return false;
3531 }
3532
3533 if ((annotation->flags() & Annotation::External) && !d->canModifyExternalAnnotations()) {
3534 return false;
3535 }
3536
3537 switch (annotation->subType()) {
3538 case Annotation::AText:
3539 case Annotation::ALine:
3540 case Annotation::AGeom:
3542 case Annotation::AStamp:
3543 case Annotation::AInk:
3545 return true;
3546 default:
3547 return false;
3548 }
3549}
3550
3552{
3553 Q_ASSERT(d->m_prevPropsOfAnnotBeingModified.isNull());
3554 if (!d->m_prevPropsOfAnnotBeingModified.isNull()) {
3555 qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties has already been called since last call to Document::modifyPageAnnotationProperties";
3556 return;
3557 }
3558 d->m_prevPropsOfAnnotBeingModified = annotation->getAnnotationPropertiesDomNode();
3559}
3560
3562{
3563 Q_ASSERT(!d->m_prevPropsOfAnnotBeingModified.isNull());
3564 if (d->m_prevPropsOfAnnotBeingModified.isNull()) {
3565 qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties must be called before Annotation is modified";
3566 return;
3567 }
3568 QDomNode prevProps = d->m_prevPropsOfAnnotBeingModified;
3569 QUndoCommand *uc = new Okular::ModifyAnnotationPropertiesCommand(d, annotation, page, prevProps, annotation->getAnnotationPropertiesDomNode());
3570 d->m_undoStack->push(uc);
3571 d->m_prevPropsOfAnnotBeingModified.clear();
3572}
3573
3574void Document::translatePageAnnotation(int page, Annotation *annotation, const NormalizedPoint &delta)
3575{
3576 int complete = (annotation->flags() & Okular::Annotation::BeingMoved) == 0;
3577 QUndoCommand *uc = new Okular::TranslateAnnotationCommand(d, annotation, page, delta, complete);
3578 d->m_undoStack->push(uc);
3579}
3580
3581void Document::adjustPageAnnotation(int page, Annotation *annotation, const Okular::NormalizedPoint &delta1, const Okular::NormalizedPoint &delta2)
3582{
3583 const bool complete = (annotation->flags() & Okular::Annotation::BeingResized) == 0;
3584 QUndoCommand *uc = new Okular::AdjustAnnotationCommand(d, annotation, page, delta1, delta2, complete);
3585 d->m_undoStack->push(uc);
3586}
3587
3588void Document::editPageAnnotationContents(int page, Annotation *annotation, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3589{
3590 QString prevContents = annotation->contents();
3591 QUndoCommand *uc = new EditAnnotationContentsCommand(d, annotation, page, newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos);
3592 d->m_undoStack->push(uc);
3593}
3594
3596{
3597 if (!annotation || (annotation->flags() & Annotation::DenyDelete)) {
3598 return false;
3599 }
3600
3601 if ((annotation->flags() & Annotation::External) && !d->canRemoveExternalAnnotations()) {
3602 return false;
3603 }
3604
3605 switch (annotation->subType()) {
3606 case Annotation::AText:
3607 case Annotation::ALine:
3608 case Annotation::AGeom:
3610 case Annotation::AStamp:
3611 case Annotation::AInk:
3612 case Annotation::ACaret:
3613 return true;
3614 default:
3615 return false;
3616 }
3617}
3618
3620{
3621 QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page);
3622 d->m_undoStack->push(uc);
3623}
3624
3626{
3627 d->m_undoStack->beginMacro(i18nc("remove a collection of annotations from the page", "remove annotations"));
3628 for (Annotation *annotation : annotations) {
3629 QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page);
3630 d->m_undoStack->push(uc);
3631 }
3632 d->m_undoStack->endMacro();
3633}
3634
3635bool DocumentPrivate::canAddAnnotationsNatively() const
3636{
3637 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3638
3640 return true;
3641 }
3642
3643 return false;
3644}
3645
3646bool DocumentPrivate::canModifyExternalAnnotations() const
3647{
3648 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3649
3651 return true;
3652 }
3653
3654 return false;
3655}
3656
3657bool DocumentPrivate::canRemoveExternalAnnotations() const
3658{
3659 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3660
3662 return true;
3663 }
3664
3665 return false;
3666}
3667
3668void Document::setPageTextSelection(int page, std::unique_ptr<RegularAreaRect> &&rect, const QColor &color)
3669{
3670 Page *kp = d->m_pagesVector[page];
3671 if (!d->m_generator || !kp) {
3672 return;
3673 }
3674
3675 // add or remove the selection basing whether rect is null or not
3676 if (rect) {
3677 kp->d->setTextSelections(*rect, color);
3678 } else {
3679 kp->d->deleteTextSelections();
3680 }
3681
3682 // notify observers about the change
3683 foreachObserver(notifyPageChanged(page, DocumentObserver::TextSelection));
3684}
3685
3687{
3688 return d->m_undoStack->canUndo();
3689}
3690
3692{
3693 return d->m_undoStack->canRedo();
3694}
3695
3696/* REFERENCE IMPLEMENTATION: better calling setViewport from other code
3697void Document::setNextPage()
3698{
3699 // advance page and set viewport on observers
3700 if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 )
3701 setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber + 1 ) );
3702}
3703
3704void Document::setPrevPage()
3705{
3706 // go to previous page and set viewport on observers
3707 if ( (*d->m_viewportIterator).pageNumber > 0 )
3708 setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber - 1 ) );
3709}
3710*/
3711
3712void Document::setViewport(const DocumentViewport &viewport, DocumentObserver *excludeObserver, bool smoothMove, bool updateHistory)
3713{
3714 if (!viewport.isValid()) {
3715 qCDebug(OkularCoreDebug) << "invalid viewport:" << viewport.toString();
3716 return;
3717 }
3718 if (viewport.pageNumber >= int(d->m_pagesVector.count())) {
3719 // qCDebug(OkularCoreDebug) << "viewport out of document:" << viewport.toString();
3720 return;
3721 }
3722
3723 // if already broadcasted, don't redo it
3724 DocumentViewport &oldViewport = *d->m_viewportIterator;
3725 // disabled by enrico on 2005-03-18 (less debug output)
3726 // if ( viewport == oldViewport )
3727 // qCDebug(OkularCoreDebug) << "setViewport with the same viewport.";
3728
3729 const int oldPageNumber = oldViewport.pageNumber;
3730
3731 // set internal viewport taking care of history
3732 if (oldViewport.pageNumber == viewport.pageNumber || !oldViewport.isValid() || !updateHistory) {
3733 // if page is unchanged save the viewport at current position in queue
3734 oldViewport = viewport;
3735 } else {
3736 // remove elements after viewportIterator in queue
3737 d->m_viewportHistory.erase(++d->m_viewportIterator, d->m_viewportHistory.end());
3738
3739 // keep the list to a reasonable size by removing head when needed
3740 if (d->m_viewportHistory.size() >= OKULAR_HISTORY_MAXSTEPS) {
3741 d->m_viewportHistory.pop_front();
3742 }
3743
3744 // add the item at the end of the queue
3745 d->m_viewportIterator = d->m_viewportHistory.insert(d->m_viewportHistory.end(), viewport);
3746 }
3747
3748 const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3749
3750 const bool currentPageChanged = (oldPageNumber != currentViewportPage);
3751
3752 // notify change to all other (different from id) observers
3753 for (DocumentObserver *o : std::as_const(d->m_observers)) {
3754 if (o != excludeObserver) {
3755 o->notifyViewportChanged(smoothMove);
3756 }
3757
3758 if (currentPageChanged) {
3759 o->notifyCurrentPageChanged(oldPageNumber, currentViewportPage);
3760 }
3761 }
3762}
3763
3764void Document::setViewportPage(int page, DocumentObserver *excludeObserver, bool smoothMove)
3765{
3766 // clamp page in range [0 ... numPages-1]
3767 if (page < 0) {
3768 page = 0;
3769 } else if (page > (int)d->m_pagesVector.count()) {
3770 page = d->m_pagesVector.count() - 1;
3771 }
3772
3773 // make a viewport from the page and broadcast it
3774 setViewport(DocumentViewport(page), excludeObserver, smoothMove);
3775}
3776
3777void Document::setZoom(int factor, DocumentObserver *excludeObserver)
3778{
3779 // notify change to all other (different from id) observers
3780 for (DocumentObserver *o : std::as_const(d->m_observers)) {
3781 if (o != excludeObserver) {
3782 o->notifyZoom(factor);
3783 }
3784 }
3785}
3786
3788// restore viewport from the history
3789{
3790 if (d->m_viewportIterator != d->m_viewportHistory.begin()) {
3791 const int oldViewportPage = (*d->m_viewportIterator).pageNumber;
3792
3793 // restore previous viewport and notify it to observers
3794 --d->m_viewportIterator;
3795 foreachObserver(notifyViewportChanged(true));
3796
3797 const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3798 if (oldViewportPage != currentViewportPage)
3799 foreachObserver(notifyCurrentPageChanged(oldViewportPage, currentViewportPage));
3800 }
3801}
3802
3804// restore next viewport from the history
3805{
3806 auto nextIterator = std::list<DocumentViewport>::const_iterator(d->m_viewportIterator);
3807 ++nextIterator;
3808 if (nextIterator != d->m_viewportHistory.end()) {
3809 const int oldViewportPage = (*d->m_viewportIterator).pageNumber;
3810
3811 // restore next viewport and notify it to observers
3812 ++d->m_viewportIterator;
3813 foreachObserver(notifyViewportChanged(true));
3814
3815 const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3816 if (oldViewportPage != currentViewportPage)
3817 foreachObserver(notifyCurrentPageChanged(oldViewportPage, currentViewportPage));
3818 }
3819}
3820
3822{
3823 d->m_nextDocumentViewport = viewport;
3824}
3825
3827{
3828 d->m_nextDocumentDestination = namedDestination;
3829}
3830
3831void Document::searchText(int searchID, const QString &text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor &color)
3832{
3833 d->m_searchCancelled = false;
3834
3835 // safety checks: don't perform searches on empty or unsearchable docs
3836 if (!d->m_generator || !d->m_generator->hasFeature(Generator::TextExtraction) || d->m_pagesVector.isEmpty()) {
3838 return;
3839 }
3840
3841 // if searchID search not recorded, create new descriptor and init params
3842 QMap<int, RunningSearch *>::iterator searchIt = d->m_searches.find(searchID);
3843 if (searchIt == d->m_searches.end()) {
3844 RunningSearch *search = new RunningSearch();
3845 search->continueOnPage = -1;
3846 searchIt = d->m_searches.insert(searchID, search);
3847 }
3848 RunningSearch *s = *searchIt;
3849
3850 // update search structure
3851 bool newText = text != s->cachedString;
3852 s->cachedString = text;
3853 s->cachedType = type;
3854 s->cachedCaseSensitivity = caseSensitivity;
3855 s->cachedViewportMove = moveViewport;
3856 s->cachedColor = color;
3857 s->isCurrentlySearching = true;
3858
3859 // global data for search
3860 QSet<int> *pagesToNotify = new QSet<int>;
3861
3862 // remove highlights from pages and queue them for notifying changes
3863 *pagesToNotify += s->highlightedPages;
3864 for (const int pageNumber : std::as_const(s->highlightedPages)) {
3865 d->m_pagesVector.at(pageNumber)->d->deleteHighlights(searchID);
3866 }
3867 s->highlightedPages.clear();
3868
3869 // set hourglass cursor
3871
3872 // 1. ALLDOC - process all document marking pages
3873 if (type == AllDocument) {
3875
3876 // search and highlight 'text' (as a solid phrase) on all pages
3877 QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID] { d->doContinueAllDocumentSearch(pagesToNotify, pageMatches, 0, searchID); });
3878 }
3879 // 2. NEXTMATCH - find next matching item (or start from top)
3880 // 3. PREVMATCH - find previous matching item (or start from bottom)
3881 else if (type == NextMatch || type == PreviousMatch) {
3882 // find out from where to start/resume search from
3883 const bool forward = type == NextMatch;
3884 const int viewportPage = (*d->m_viewportIterator).pageNumber;
3885 const int fromStartSearchPage = forward ? 0 : d->m_pagesVector.count() - 1;
3886 int currentPage = fromStart ? fromStartSearchPage : ((s->continueOnPage != -1) ? s->continueOnPage : viewportPage);
3887 const Page *lastPage = fromStart ? nullptr : d->m_pagesVector[currentPage];
3888 int pagesDone = 0;
3889
3890 // continue checking last TextPage first (if it is the current page)
3891 RegularAreaRect *match = nullptr;
3892 if (lastPage && lastPage->number() == s->continueOnPage) {
3893 if (newText) {
3894 match = lastPage->findText(searchID, text, forward ? FromTop : FromBottom, caseSensitivity);
3895 } else {
3896 match = lastPage->findText(searchID, text, forward ? NextResult : PreviousResult, caseSensitivity, &s->continueOnMatch);
3897 }
3898 if (!match) {
3899 if (forward) {
3900 currentPage++;
3901 } else {
3902 currentPage--;
3903 }
3904 pagesDone++;
3905 }
3906 }
3907
3908 s->pagesDone = pagesDone;
3909
3910 DoContinueDirectionMatchSearchStruct *searchStruct = new DoContinueDirectionMatchSearchStruct();
3911 searchStruct->pagesToNotify = pagesToNotify;
3912 searchStruct->match = match;
3913 searchStruct->currentPage = currentPage;
3914 searchStruct->searchID = searchID;
3915
3916 QTimer::singleShot(0, this, [this, searchStruct] { d->doContinueDirectionMatchSearch(searchStruct); });
3917 }
3918 // 4. GOOGLE* - process all document marking pages
3919 else if (type == GoogleAll || type == GoogleAny) {
3921 const QStringList words = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
3922
3923 // search and highlight every word in 'text' on all pages
3924 QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID, words] { d->doContinueGooglesDocumentSearch(pagesToNotify, pageMatches, 0, searchID, words); });
3925 }
3926}
3927
3929{
3930 // check if searchID is present in runningSearches
3931 QMap<int, RunningSearch *>::const_iterator it = d->m_searches.constFind(searchID);
3932 if (it == d->m_searches.constEnd()) {
3934 return;
3935 }
3936
3937 // start search with cached parameters from last search by searchID
3938 RunningSearch *p = *it;
3939 if (!p->isCurrentlySearching) {
3940 searchText(searchID, p->cachedString, false, p->cachedCaseSensitivity, p->cachedType, p->cachedViewportMove, p->cachedColor);
3941 }
3942}
3943
3944void Document::continueSearch(int searchID, SearchType type)
3945{
3946 // check if searchID is present in runningSearches
3947 QMap<int, RunningSearch *>::const_iterator it = d->m_searches.constFind(searchID);
3948 if (it == d->m_searches.constEnd()) {
3950 return;
3951 }
3952
3953 // start search with cached parameters from last search by searchID
3954 RunningSearch *p = *it;
3955 if (!p->isCurrentlySearching) {
3956 searchText(searchID, p->cachedString, false, p->cachedCaseSensitivity, type, p->cachedViewportMove, p->cachedColor);
3957 }
3958}
3959
3960void Document::resetSearch(int searchID)
3961{
3962 // if we are closing down, don't bother doing anything
3963 if (!d->m_generator) {
3964 return;
3965 }
3966
3967 // check if searchID is present in runningSearches
3968 QMap<int, RunningSearch *>::iterator searchIt = d->m_searches.find(searchID);
3969 if (searchIt == d->m_searches.end()) {
3970 return;
3971 }
3972
3973 // get previous parameters for search
3974 RunningSearch *s = *searchIt;
3975
3976 // unhighlight pages and inform observers about that
3977 for (const int pageNumber : std::as_const(s->highlightedPages)) {
3978 d->m_pagesVector.at(pageNumber)->d->deleteHighlights(searchID);
3979 foreachObserver(notifyPageChanged(pageNumber, DocumentObserver::Highlights));
3980 }
3981
3982 // send the setup signal too (to update views that filter on matches)
3983 foreachObserver(notifySetup(d->m_pagesVector, 0));
3984
3985 // remove search from the runningSearches list and delete it
3986 d->m_searches.erase(searchIt);
3987 delete s;
3988}
3989
3991{
3992 d->m_searchCancelled = true;
3993}
3994
3996{
3997 d->m_undoStack->undo();
3998}
3999
4001{
4002 d->m_undoStack->redo();
4003}
4004
4005void Document::editFormText(int pageNumber, Okular::FormFieldText *form, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
4006{
4007 QUndoCommand *uc = new EditFormTextCommand(this->d, form, pageNumber, newContents, newCursorPos, form->text(), prevCursorPos, prevAnchorPos);
4008 d->m_undoStack->push(uc);
4009}
4010
4011void Document::editFormText(int pageNumber, Okular::FormFieldText *form, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos, const QString &oldContents)
4012{
4013 QUndoCommand *uc = new EditFormTextCommand(this->d, form, pageNumber, newContents, newCursorPos, oldContents, prevCursorPos, prevAnchorPos);
4014 d->m_undoStack->push(uc);
4015}
4016
4017void Document::editFormList(int pageNumber, FormFieldChoice *form, const QList<int> &newChoices)
4018{
4019 const QList<int> prevChoices = form->currentChoices();
4020 QUndoCommand *uc = new EditFormListCommand(this->d, form, pageNumber, newChoices, prevChoices);
4021 d->m_undoStack->push(uc);
4022}
4023
4024void Document::editFormCombo(int pageNumber, FormFieldChoice *form, const QString &newText, int newCursorPos, int prevCursorPos, int prevAnchorPos)
4025{
4026 QString prevText;
4027 if (form->currentChoices().isEmpty()) {
4028 prevText = form->editChoice();
4029 } else {
4030 prevText = form->choices().at(form->currentChoices().constFirst());
4031 }
4032
4033 QUndoCommand *uc = new EditFormComboCommand(this->d, form, pageNumber, newText, newCursorPos, prevText, prevCursorPos, prevAnchorPos);
4034 d->m_undoStack->push(uc);
4035}
4036
4037void Document::editFormButtons(int pageNumber, const QList<FormFieldButton *> &formButtons, const QList<bool> &newButtonStates)
4038{
4039 QUndoCommand *uc = new EditFormButtonsCommand(this->d, pageNumber, formButtons, newButtonStates);
4040 d->m_undoStack->push(uc);
4041}
4042
4044{
4045 const int numOfPages = pages();
4046 for (int i = currentPage(); i >= 0; i--) {
4047 d->refreshPixmaps(i);
4048 }
4049 for (int i = currentPage() + 1; i < numOfPages; i++) {
4050 d->refreshPixmaps(i);
4051 }
4052}
4053
4055{
4056 return d->m_bookmarkManager;
4057}
4058
4060{
4061 QList<int> list;
4062 uint docPages = pages();
4063
4064 // pages are 0-indexed internally, but 1-indexed externally
4065 for (uint i = 0; i < docPages; i++) {
4066 if (bookmarkManager()->isBookmarked(i)) {
4067 list << i + 1;
4068 }
4069 }
4070 return list;
4071}
4072
4074{
4075 // Code formerly in Part::slotPrint()
4076 // range detecting
4077 QString range;
4078 uint docPages = pages();
4079 int startId = -1;
4080 int endId = -1;
4081
4082 for (uint i = 0; i < docPages; ++i) {
4083 if (bookmarkManager()->isBookmarked(i)) {
4084 if (startId < 0) {
4085 startId = i;
4086 }
4087 if (endId < 0) {
4088 endId = startId;
4089 } else {
4090 ++endId;
4091 }
4092 } else if (startId >= 0 && endId >= 0) {
4093 if (!range.isEmpty()) {
4094 range += QLatin1Char(',');
4095 }
4096
4097 if (endId - startId > 0) {
4098 range += QStringLiteral("%1-%2").arg(startId + 1).arg(endId + 1);
4099 } else {
4100 range += QString::number(startId + 1);
4101 }
4102 startId = -1;
4103 endId = -1;
4104 }
4105 }
4106 if (startId >= 0 && endId >= 0) {
4107 if (!range.isEmpty()) {
4108 range += QLatin1Char(',');
4109 }
4110
4111 if (endId - startId > 0) {
4112 range += QStringLiteral("%1-%2").arg(startId + 1).arg(endId + 1);
4113 } else {
4114 range += QString::number(startId + 1);
4115 }
4116 }
4117 return range;
4118}
4119
4120struct ExecuteNextActionsHelper : public QObject, private DocumentObserver {
4121 Q_OBJECT
4122public:
4123 explicit ExecuteNextActionsHelper(Document *doc)
4124 : m_doc(doc)
4125 {
4126 doc->addObserver(this);
4127 connect(doc, &Document::aboutToClose, this, [this] { b = false; });
4128 }
4129
4130 ~ExecuteNextActionsHelper() override
4131 {
4132 m_doc->removeObserver(this);
4133 }
4134
4135 void notifySetup(const QVector<Okular::Page *> & /*pages*/, int setupFlags) override
4136 {
4137 if (setupFlags == DocumentChanged || setupFlags == UrlChanged) {
4138 b = false;
4139 }
4140 }
4141
4142 bool shouldExecuteNextAction() const
4143 {
4144 return b;
4145 }
4146
4147private:
4148 Document *const m_doc;
4149 bool b = true;
4150};
4151
4153{
4154 if (!action) {
4155 return;
4156 }
4157
4158 // Don't execute next actions if the action itself caused the closing of the document
4159 const ExecuteNextActionsHelper executeNextActionsHelper(this);
4160
4161 switch (action->actionType()) {
4162 case Action::Goto: {
4163 const GotoAction *go = static_cast<const GotoAction *>(action);
4164 d->m_nextDocumentViewport = go->destViewport();
4165 d->m_nextDocumentDestination = go->destinationName();
4166
4167 // Explanation of why d->m_nextDocumentViewport is needed:
4168 // all openRelativeFile does is launch a signal telling we
4169 // want to open another URL, the problem is that when the file is
4170 // non local, the loading is done asynchronously so you can't
4171 // do a setViewport after the if as it was because you are doing the setViewport
4172 // on the old file and when the new arrives there is no setViewport for it and
4173 // it does not show anything
4174
4175 // first open filename if link is pointing outside this document
4176 const QString filename = go->fileName();
4177 if (go->isExternal() && !d->openRelativeFile(filename)) {
4178 qCWarning(OkularCoreDebug).nospace() << "Action: Error opening '" << filename << "'.";
4179 break;
4180 } else {
4181 const DocumentViewport nextViewport = d->nextDocumentViewport();
4182 // skip local links that point to nowhere (broken ones)
4183 if (!nextViewport.isValid()) {
4184 break;
4185 }
4186
4187 setViewport(nextViewport, nullptr, true);
4188 d->m_nextDocumentViewport = DocumentViewport();
4189 d->m_nextDocumentDestination = QString();
4190 }
4191
4192 } break;
4193
4194 case Action::Execute: {
4195 const ExecuteAction *exe = static_cast<const ExecuteAction *>(action);
4196 const QString fileName = exe->fileName();
4197 if (fileName.endsWith(QLatin1String(".pdf"), Qt::CaseInsensitive)) {
4198 d->openRelativeFile(fileName);
4199 break;
4200 }
4201
4202 // Albert: the only pdf i have that has that kind of link don't define
4203 // an application and use the fileName as the file to open
4204 QUrl url = d->giveAbsoluteUrl(fileName);
4205 QMimeDatabase db;
4206 QMimeType mime = db.mimeTypeForUrl(url);
4207 // Check executables
4208 if (KIO::OpenUrlJob::isExecutableFile(url, mime.name())) {
4209 // Don't have any pdf that uses this code path, just a guess on how it should work
4210 if (!exe->parameters().isEmpty()) {
4211 url = d->giveAbsoluteUrl(exe->parameters());
4212 mime = db.mimeTypeForUrl(url);
4213
4214 if (KIO::OpenUrlJob::isExecutableFile(url, mime.name())) {
4215 // this case is a link pointing to an executable with a parameter
4216 // that also is an executable, possibly a hand-crafted pdf
4217 Q_EMIT error(i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that."), -1);
4218 break;
4219 }
4220 } else {
4221 // this case is a link pointing to an executable with no parameters
4222 // core developers find unacceptable executing it even after asking the user
4223 Q_EMIT error(i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that."), -1);
4224 break;
4225 }
4226 }
4227
4228 KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mime.name());
4230 job->start();
4231 connect(job, &KIO::OpenUrlJob::result, this, [this, mime](KJob *job) {
4232 if (job->error()) {
4233 Q_EMIT error(i18n("No application found for opening file of mimetype %1.", mime.name()), -1);
4234 }
4235 });
4236 } break;
4237
4238 case Action::DocAction: {
4239 const DocumentAction *docaction = static_cast<const DocumentAction *>(action);
4240 switch (docaction->documentActionType()) {
4242 setViewportPage(0);
4243 break;
4245 if ((*d->m_viewportIterator).pageNumber > 0) {
4246 setViewportPage((*d->m_viewportIterator).pageNumber - 1);
4247 }
4248 break;
4250 if ((*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1) {
4251 setViewportPage((*d->m_viewportIterator).pageNumber + 1);
4252 }
4253 break;
4255 setViewportPage(d->m_pagesVector.count() - 1);
4256 break;
4259 break;
4262 break;
4264 Q_EMIT quit();
4265 break;
4268 break;
4271 break;
4273 Q_EMIT linkFind();
4274 break;
4277 break;
4279 Q_EMIT close();
4280 break;
4283 break;
4286 break;
4287 }
4288 } break;
4289
4290 case Action::Browse: {
4291 const BrowseAction *browse = static_cast<const BrowseAction *>(action);
4292 QString lilySource;
4293 int lilyRow = 0, lilyCol = 0;
4294 // if the url is a mailto one, invoke mailer
4295 if (browse->url().scheme() == QLatin1String("mailto")) {
4296 QDesktopServices::openUrl(browse->url());
4297 } else if (extractLilyPondSourceReference(browse->url(), &lilySource, &lilyRow, &lilyCol)) {
4298 const SourceReference ref(lilySource, lilyRow, lilyCol);
4300 } else {
4301 const QUrl url = browse->url();
4302
4303 // fix for #100366, documents with relative links that are the form of http:foo.pdf
4304 if ((url.scheme() == QLatin1String("http")) && url.host().isEmpty() && url.fileName().endsWith(QLatin1String("pdf"))) {
4305 d->openRelativeFile(url.fileName());
4306 break;
4307 }
4308
4309 // handle documents with relative path
4310 QUrl realUrl;
4311 if (d->m_url.isValid()) {
4312 realUrl = KIO::upUrl(d->m_url).resolved(url);
4313 } else if (!url.isRelative()) {
4314 realUrl = url;
4315 }
4316 if (realUrl.isValid()) {
4317 auto *job = new KIO::OpenUrlJob(realUrl);
4318 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->m_widget.data()));
4319 job->start();
4320 }
4321 }
4322 } break;
4323
4324 case Action::Sound: {
4325 const SoundAction *linksound = static_cast<const SoundAction *>(action);
4326 AudioPlayer::instance()->playSound(linksound->sound(), linksound);
4327 } break;
4328
4329 case Action::Script: {
4330 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4331 if (!d->m_scripter) {
4332 d->m_scripter = new Scripter(d);
4333 }
4334 d->m_scripter->execute(linkscript->scriptType(), linkscript->script());
4335 } break;
4336
4337 case Action::Movie:
4338 Q_EMIT processMovieAction(static_cast<const MovieAction *>(action));
4339 break;
4340 case Action::Rendition: {
4341 const RenditionAction *linkrendition = static_cast<const RenditionAction *>(action);
4342 if (!linkrendition->script().isEmpty()) {
4343 if (!d->m_scripter) {
4344 d->m_scripter = new Scripter(d);
4345 }
4346 d->m_scripter->execute(linkrendition->scriptType(), linkrendition->script());
4347 }
4348
4349 Q_EMIT processRenditionAction(static_cast<const RenditionAction *>(action));
4350 } break;
4351 case Action::BackendOpaque: {
4352 const BackendOpaqueAction *backendOpaqueAction = static_cast<const BackendOpaqueAction *>(action);
4353 Okular::BackendOpaqueAction::OpaqueActionResult res = d->m_generator->opaqueAction(backendOpaqueAction);
4354 if (res & Okular::BackendOpaqueAction::RefreshForms) {
4355 for (const Page *p : std::as_const(d->m_pagesVector)) {
4356 const QList<Okular::FormField *> forms = p->formFields();
4357 for (FormField *form : forms) {
4359 }
4360 d->refreshPixmaps(p->number());
4361 }
4362 }
4363 } break;
4364 }
4365
4366 if (executeNextActionsHelper.shouldExecuteNextAction()) {
4367 const QVector<Action *> nextActions = action->nextActions();
4368 for (const Action *a : nextActions) {
4369 processAction(a);
4370 }
4371 }
4372}
4373
4375{
4376 processFormatAction(action, static_cast<FormField *>(fft));
4377}
4378
4380{
4381 if (action->actionType() != Action::Script) {
4382 qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for formatting.";
4383 return;
4384 }
4385
4386 // Lookup the page of the FormFieldText
4387 int foundPage = d->findFieldPageNumber(ff);
4388
4389 if (foundPage == -1) {
4390 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4391 return;
4392 }
4393
4394 const QString unformattedText = ff->value().toString();
4395
4396 std::shared_ptr<Event> event = Event::createFormatEvent(ff, d->m_pagesVector[foundPage]);
4397
4398 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4399
4400 d->executeScriptEvent(event, linkscript);
4401
4402 const QString formattedText = event->value().toString();
4403 ff->commitFormattedValue(formattedText);
4404 if (formattedText != unformattedText) {
4405 // We set the formattedText, because when we call refreshFormWidget
4406 // It will set the QLineEdit to this formattedText
4407 ff->setValue(QVariant(formattedText));
4408 ff->setAppearanceValue(QVariant(formattedText));
4410 d->refreshPixmaps(foundPage);
4411 // Then we make the form have the unformatted text, to use
4412 // in calculations and other things
4413 ff->setValue(QVariant(unformattedText));
4415 // When the field was calculated we need to refresh even
4416 // if the format script changed nothing. e.g. on error.
4417 // This is because the recalculateForms function delegated
4418 // the responsiblity for the refresh to us.
4420 d->refreshPixmaps(foundPage);
4421 }
4422}
4423
4424QString DocumentPrivate::evaluateKeystrokeEventChange(const QString &oldVal, const QString &newVal, int selStart, int selEnd)
4425{
4426 /*
4427 The change needs to be evaluated here in accordance with code points.
4428 selStart and selEnd parameters passed to this method should be been adjusted accordingly.
4429
4430 Since QString methods work in terms of code units, we convert the strings to UTF-32.
4431 */
4432 std::u32string oldUcs4 = oldVal.toStdU32String();
4433 std::u32string newUcs4 = newVal.toStdU32String();
4434 if (selStart < 0 || selEnd < 0 || (selEnd - selStart) + (static_cast<int>(newUcs4.size()) - static_cast<int>(oldUcs4.size())) < 0) {
4435 // Prevent Okular from crashing if incorrect parameters are passed or some bug causes incorrect calculation
4436 return {};
4437 }
4438 const size_t changeLength = (selEnd - selStart) + (newUcs4.size() - oldUcs4.size());
4439 auto subview = std::u32string_view {newUcs4}.substr(selStart, changeLength);
4440 if (subview.empty()) {
4441 // If subview is empty (in scenarios when selStart is at end and changeLength is non-zero) fromUcs4 returns \u0000.
4442 // This should not happen, but just a guard.
4443 return {};
4444 }
4445 Q_ASSERT(subview.length() == changeLength);
4446 return QString::fromUcs4(subview.data(), subview.length());
4447}
4448
4449void Document::processKeystrokeAction(const Action *action, Okular::FormField *ff, const QVariant &newValue, int prevCursorPos, int prevAnchorPos)
4450{
4451 if (action->actionType() != Action::Script) {
4452 qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for keystroke.";
4453 return;
4454 }
4455 // Lookup the page of the FormFieldText
4456 int foundPage = d->findFieldPageNumber(ff);
4457
4458 if (foundPage == -1) {
4459 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4460 return;
4461 }
4462
4463 std::shared_ptr<Event> event = Event::createKeystrokeEvent(ff, d->m_pagesVector[foundPage]);
4464
4465 /* Set the selStart and selEnd event properties
4466
4467 QString using UTF-16 counts a code point as made up of 1 or 2 16-bit code units.
4468
4469 When encoded using 2 code units, the units are referred to as surrogate pairs.
4470 selectionStart() and selectionEnd() methods evaluate prevCursorPos and prevAnchorPos based on code units during selection.
4471
4472 While this unit-based evaulation is suitable for detecting changes, for providing consistency with Adobe Reader for values of selStart and selEnd,
4473 it would be best to evaluate in terms of code points rather than the code units.
4474
4475 To correct the values of selStart and selEnd accordingly, we iterate over the code units. If a surrogate pair is encountered, then selStart and
4476 selEnd are accordingly decremented.
4477 */
4478 int selStart = std::min(prevCursorPos, prevAnchorPos);
4479 int selEnd = std::max(prevCursorPos, prevAnchorPos);
4480 int codeUnit;
4481 int initialSelStart = selStart;
4482 int initialSelEnd = selEnd;
4483 QString inputString = ff->value().toString();
4484 for (codeUnit = 0; codeUnit < initialSelStart && codeUnit < inputString.size(); codeUnit++) {
4485 if (inputString.at(codeUnit).isHighSurrogate()) {
4486 // skip the low surrogate and decrement selStart and selEnd
4487 codeUnit++;
4488 selStart--;
4489 selEnd--;
4490 }
4491 }
4492 for (; codeUnit < initialSelEnd && codeUnit < inputString.size(); codeUnit++) {
4493 if (inputString.at(codeUnit).isHighSurrogate()) {
4494 // skip the low surrogate and decrement selEnd
4495 codeUnit++;
4496 selEnd--;
4497 }
4498 }
4499 std::u32string oldUcs4 = inputString.toStdU32String();
4500 std::u32string newUcs4 = newValue.toString().toStdU32String();
4501 // It is necessary to count size in terms of code points rather than code units for deletion.
4502 if (oldUcs4.size() - newUcs4.size() == 1 && selStart == selEnd) {
4503 // consider a one character removal as selection of that character and then its removal.
4504 selStart--;
4505 }
4506 event->setSelStart(selStart);
4507 event->setSelEnd(selEnd);
4508 // Use the corrected selStart and selEnd for evaluating the change.
4509 event->setChange(DocumentPrivate::evaluateKeystrokeEventChange(inputString, newValue.toString(), selStart, selEnd));
4510 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4511
4512 d->executeScriptEvent(event, linkscript);
4513
4514 if (event->returnCode()) {
4515 ff->setValue(newValue);
4516 } else {
4518 }
4519}
4520
4522{
4523 // use -1 as default
4524 processKeystrokeAction(action, fft, newValue, -1, -1);
4525}
4526
4528{
4529 bool returnCode = false;
4530 processKeystrokeCommitAction(action, fft, returnCode);
4531}
4532
4533void Document::processKeystrokeCommitAction(const Action *action, Okular::FormField *ff, bool &returnCode)
4534{
4535 if (action->actionType() != Action::Script) {
4536 qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for keystroke.";
4537 return;
4538 }
4539 // Lookup the page of the FormFieldText
4540 int foundPage = d->findFieldPageNumber(ff);
4541
4542 if (foundPage == -1) {
4543 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4544 return;
4545 }
4546
4547 std::shared_ptr<Event> event = Event::createKeystrokeEvent(ff, d->m_pagesVector[foundPage]);
4548 event->setWillCommit(true);
4549
4550 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4551
4552 d->executeScriptEvent(event, linkscript);
4553
4554 if (!event->returnCode()) {
4557 ff->setValue(QVariant(ff->committedValue()));
4558 } else {
4559 ff->setValue(QVariant(event->value().toString()));
4561 }
4562 returnCode = event->returnCode();
4563}
4564
4566{
4567 if (!action || action->actionType() != Action::Script) {
4568 return;
4569 }
4570
4571 // Lookup the page of the FormFieldText
4572 int foundPage = d->findFieldPageNumber(field);
4573
4574 if (foundPage == -1) {
4575 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4576 return;
4577 }
4578
4579 std::shared_ptr<Event> event = Event::createFormFocusEvent(field, d->m_pagesVector[foundPage]);
4580
4581 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4582
4583 d->executeScriptEvent(event, linkscript);
4584}
4585
4586void Document::processValidateAction(const Action *action, Okular::FormFieldText *fft, bool &returnCode)
4587{
4588 processValidateAction(action, static_cast<FormField *>(fft), returnCode);
4589}
4590
4591void Document::processValidateAction(const Action *action, Okular::FormField *ff, bool &returnCode)
4592{
4593 if (!action || action->actionType() != Action::Script) {
4594 return;
4595 }
4596
4597 // Lookup the page of the FormFieldText
4598 int foundPage = d->findFieldPageNumber(ff);
4599
4600 if (foundPage == -1) {
4601 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4602 return;
4603 }
4604
4605 std::shared_ptr<Event> event = Event::createFormValidateEvent(ff, d->m_pagesVector[foundPage]);
4606
4607 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4608
4609 d->executeScriptEvent(event, linkscript);
4610 if (!event->returnCode()) {
4613 ff->setValue(QVariant(ff->committedValue()));
4614 } else {
4615 ff->setValue(QVariant(event->value().toString()));
4617 }
4618 returnCode = event->returnCode();
4619}
4620
4622{
4623 if (ff->value().toString() == ff->committedValue()) {
4626 ff->setValue(QVariant(ff->committedValue()));
4627 return;
4628 }
4629
4630 bool returnCode = true;
4633 }
4634
4636 if (returnCode) {
4637 processValidateAction(action, ff, returnCode);
4638 }
4639 }
4640
4641 if (!returnCode) {
4642 return;
4643 } else {
4644 ff->commitValue(ff->value().toString());
4645 }
4646
4648
4650 processFormatAction(action, ff);
4651 } else {
4652 ff->commitFormattedValue(ff->value().toString());
4653 }
4654}
4655
4657{
4658 if (!action || action->actionType() != Action::Script) {
4659 return;
4660 }
4661
4662 Event::EventType eventType = Okular::Event::UnknownEvent;
4663
4664 switch (type) {
4665 case Document::CloseDocument:
4666 eventType = Okular::Event::DocWillClose;
4667 break;
4668 case Document::SaveDocumentStart:
4669 eventType = Okular::Event::DocWillSave;
4670 break;
4671 case Document::SaveDocumentFinish:
4672 eventType = Okular::Event::DocDidSave;
4673 break;
4674 case Document::PrintDocumentStart:
4675 eventType = Okular::Event::DocWillPrint;
4676 break;
4677 case Document::PrintDocumentFinish:
4678 eventType = Okular::Event::DocDidPrint;
4679 break;
4680 }
4681
4682 std::shared_ptr<Event> event = Event::createDocEvent(eventType);
4683
4684 const ScriptAction *linkScript = static_cast<const ScriptAction *>(action);
4685
4686 d->executeScriptEvent(event, linkScript);
4687}
4688
4690{
4691 if (!action || action->actionType() != Action::Script) {
4692 return;
4693 }
4694
4695 Okular::Event::EventType eventType = Okular::Event::UnknownEvent;
4696
4697 switch (fieldMouseEventType) {
4698 case Document::FieldMouseDown:
4699 eventType = Okular::Event::FieldMouseDown;
4700 break;
4702 eventType = Okular::Event::FieldMouseEnter;
4703 break;
4705 eventType = Okular::Event::FieldMouseExit;
4706 break;
4708 eventType = Okular::Event::FieldMouseUp;
4709 break;
4710 }
4711
4712 // Lookup the page of the FormFieldText
4713 int foundPage = d->findFieldPageNumber(ff);
4714
4715 if (foundPage == -1) {
4716 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4717 return;
4718 }
4719
4720 std::shared_ptr<Event> event = Event::createFieldMouseEvent(ff, d->m_pagesVector[foundPage], eventType);
4721
4722 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4723
4724 d->executeScriptEvent(event, linkscript);
4725}
4726
4731
4733{
4734 if (!ref) {
4735 return;
4736 }
4737
4738 const QUrl url = d->giveAbsoluteUrl(ref->fileName());
4739 if (!url.isLocalFile()) {
4740 qCDebug(OkularCoreDebug) << url.url() << "is not a local file.";
4741 return;
4742 }
4743
4744 const QString absFileName = url.toLocalFile();
4745 if (!QFile::exists(absFileName)) {
4746 qCDebug(OkularCoreDebug) << "No such file:" << absFileName;
4747 return;
4748 }
4749
4750 bool handled = false;
4751 Q_EMIT sourceReferenceActivated(absFileName, ref->row(), ref->column(), &handled);
4752 if (handled) {
4753 return;
4754 }
4755
4756 static QHash<int, QString> editors;
4757 // init the editors table if empty (on first run, usually)
4758 if (editors.isEmpty()) {
4759 editors = buildEditorsMap();
4760 }
4761
4762 // prefer the editor from the command line
4763 QString p = d->editorCommandOverride;
4764 if (p.isEmpty()) {
4765 QHash<int, QString>::const_iterator it = editors.constFind(SettingsCore::externalEditor());
4766 if (it != editors.constEnd()) {
4767 p = *it;
4768 } else {
4769 p = SettingsCore::externalEditorCommand();
4770 }
4771 }
4772 // custom editor not yet configured
4773 if (p.isEmpty()) {
4774 return;
4775 }
4776
4777 // manually append the %f placeholder if not specified
4778 if (p.indexOf(QLatin1String("%f")) == -1) {
4779 p.append(QLatin1String(" %f"));
4780 }
4781
4782 // replacing the placeholders
4784 map.insert(QLatin1Char('f'), absFileName);
4785 map.insert(QLatin1Char('c'), QString::number(ref->column()));
4786 map.insert(QLatin1Char('l'), QString::number(ref->row()));
4788 if (cmd.isEmpty()) {
4789 return;
4790 }
4791 QStringList args = KShell::splitArgs(cmd);
4792 if (args.isEmpty()) {
4793 return;
4794 }
4795
4796 const QString prog = args.takeFirst();
4797 // Make sure prog is in PATH and not just in the CWD
4798 const QString progFullPath = QStandardPaths::findExecutable(prog);
4799 if (progFullPath.isEmpty()) {
4800 return;
4801 }
4802
4803 KProcess::startDetached(progFullPath, args);
4804}
4805
4806const SourceReference *Document::dynamicSourceReference(int pageNr, double absX, double absY)
4807{
4808 if (!d->m_synctex_scanner) {
4809 return nullptr;
4810 }
4811
4812 const QSizeF dpi = d->m_generator->dpi();
4813
4814 if (synctex_edit_query(d->m_synctex_scanner, pageNr + 1, absX * 72. / dpi.width(), absY * 72. / dpi.height()) > 0) {
4815 synctex_node_p node;
4816 // TODO what should we do if there is really more than one node?
4817 while ((node = synctex_scanner_next_result(d->m_synctex_scanner))) {
4818 int line = synctex_node_line(node);
4819 int col = synctex_node_column(node);
4820 // column extraction does not seem to be implemented in synctex so far. set the SourceReference default value.
4821 if (col == -1) {
4822 col = 0;
4823 }
4824 const char *name = synctex_scanner_get_name(d->m_synctex_scanner, synctex_node_tag(node));
4825
4826 return new Okular::SourceReference(QFile::decodeName(name), line, col);
4827 }
4828 }
4829 return nullptr;
4830}
4831
4833{
4834 if (d->m_generator) {
4835 if (d->m_generator->hasFeature(Generator::PrintNative)) {
4836 return NativePrinting;
4837 }
4838
4839#ifndef Q_OS_WIN
4840 if (d->m_generator->hasFeature(Generator::PrintPostscript)) {
4841 return PostscriptPrinting;
4842 }
4843#endif
4844 }
4845
4846 return NoPrinting;
4847}
4848
4850{
4851 return d->m_generator ? d->m_generator->hasFeature(Generator::PrintToFile) : false;
4852}
4853
4855{
4856 if (const Okular::Action *action = d->m_generator->additionalDocumentAction(PrintDocumentStart)) {
4857 processDocumentAction(action, PrintDocumentStart);
4858 }
4859 const Document::PrintError printError = d->m_generator ? d->m_generator->print(printer) : Document::UnknownPrintError;
4860 if (printError == Document::NoPrintError) {
4861 if (const Okular::Action *action = d->m_generator->additionalDocumentAction(PrintDocumentFinish)) {
4862 processDocumentAction(action, PrintDocumentFinish);
4863 }
4864 }
4865 return printError;
4866}
4867
4869{
4870 switch (error) {
4871 case TemporaryFileOpenPrintError:
4872 return i18n("Could not open a temporary file");
4873 case FileConversionPrintError:
4874 return i18n("Print conversion failed");
4875 case PrintingProcessCrashPrintError:
4876 return i18n("Printing process crashed");
4877 case PrintingProcessStartPrintError:
4878 return i18n("Printing process could not start");
4879 case PrintToFilePrintError:
4880 return i18n("Printing to file failed");
4881 case InvalidPrinterStatePrintError:
4882 return i18n("Printer was in invalid state");
4883 case UnableToFindFilePrintError:
4884 return i18n("Unable to find file to print");
4885 case NoFileToPrintError:
4886 return i18n("There was no file to print");
4887 case NoBinaryToPrintError:
4888 return i18n("Could not find a suitable binary for printing. Make sure CUPS lpr binary is available");
4889 case InvalidPageSizePrintError:
4890 return i18n("The page print size is invalid");
4891 case NoPrintError:
4892 return QString();
4893 case UnknownPrintError:
4894 return QString();
4895 }
4896
4897 return QString();
4898}
4899
4901{
4902 if (d->m_generator) {
4904 return iface ? iface->printConfigurationWidget() : nullptr;
4905 } else {
4906 return nullptr;
4907 }
4908}
4909
4911{
4912 if (!dialog) {
4913 return;
4914 }
4915
4916 // We know it's a BackendConfigDialog, but check anyway
4917 BackendConfigDialog *bcd = dynamic_cast<BackendConfigDialog *>(dialog);
4918 if (!bcd) {
4919 return;
4920 }
4921
4922 // ensure that we have all the generators with settings loaded
4923 QVector<KPluginMetaData> offers = DocumentPrivate::configurableGenerators();
4924 d->loadServiceList(offers);
4925
4926 // We want the generators to be sorted by name so let's fill in a QMap
4927 // this sorts by internal id which is not awesome, but at least the sorting
4928 // is stable between runs that before it wasn't
4929 QMap<QString, GeneratorInfo> sortedGenerators;
4930 QHash<QString, GeneratorInfo>::iterator it = d->m_loadedGenerators.begin();
4931 QHash<QString, GeneratorInfo>::iterator itEnd = d->m_loadedGenerators.end();
4932 for (; it != itEnd; ++it) {
4933 sortedGenerators.insert(it.key(), it.value());
4934 }
4935
4936 bool pagesAdded = false;
4937 QMap<QString, GeneratorInfo>::iterator sit = sortedGenerators.begin();
4938 QMap<QString, GeneratorInfo>::iterator sitEnd = sortedGenerators.end();
4939 for (; sit != sitEnd; ++sit) {
4940 Okular::ConfigInterface *iface = d->generatorConfig(sit.value());
4941 if (iface) {
4942 iface->addPages(dialog);
4943 pagesAdded = true;
4944
4945 if (sit.value().generator == d->m_generator) {
4946 const int rowCount = bcd->thePageWidget()->model()->rowCount();
4947 KPageView *view = bcd->thePageWidget();
4948 view->setCurrentPage(view->model()->index(rowCount - 1, 0));
4949 }
4950 }
4951 }
4952 if (pagesAdded) {
4953 connect(dialog, &KConfigDialog::settingsChanged, this, [this] { d->slotGeneratorConfigChanged(); });
4954 }
4955}
4956
4957QVector<KPluginMetaData> DocumentPrivate::configurableGenerators()
4958{
4959 const QVector<KPluginMetaData> available = availableGenerators();
4961 for (const KPluginMetaData &md : available) {
4962 if (md.rawData().value(QStringLiteral("X-KDE-okularHasInternalSettings")).toBool()) {
4963 result << md;
4964 }
4965 }
4966 return result;
4967}
4968
4970{
4971 if (!d->m_generator) {
4972 return KPluginMetaData();
4973 }
4974
4975 auto genIt = d->m_loadedGenerators.constFind(d->m_generatorName);
4976 Q_ASSERT(genIt != d->m_loadedGenerators.constEnd());
4977 return genIt.value().metadata;
4978}
4979
4981{
4982 return DocumentPrivate::configurableGenerators().size();
4983}
4984
4986{
4987 // TODO: make it a static member of DocumentPrivate?
4988 QStringList result = d->m_supportedMimeTypes;
4989 if (result.isEmpty()) {
4990 const QVector<KPluginMetaData> available = DocumentPrivate::availableGenerators();
4991 for (const KPluginMetaData &md : available) {
4992 result << md.mimeTypes();
4993 }
4994
4995 // Remove duplicate mimetypes represented by different names
4996 QMimeDatabase mimeDatabase;
4997 QSet<QMimeType> uniqueMimetypes;
4998 for (const QString &mimeName : std::as_const(result)) {
4999 uniqueMimetypes.insert(mimeDatabase.mimeTypeForName(mimeName));
5000 }
5001 result.clear();
5002 for (const QMimeType &mimeType : uniqueMimetypes) {
5003 result.append(mimeType.name());
5004 }
5005
5006 // Add the Okular archive mimetype
5007 result << QStringLiteral("application/vnd.kde.okular-archive");
5008
5009 // Sorting by mimetype name doesn't make a ton of sense,
5010 // but ensures that the list is ordered the same way every time
5011 std::sort(result.begin(), result.end());
5012
5013 d->m_supportedMimeTypes = result;
5014 }
5015 return result;
5016}
5017
5019{
5020 if (!d->m_generator) {
5021 return false;
5022 }
5023
5024 return d->m_generator->hasFeature(Generator::SwapBackingFile);
5025}
5026
5027bool Document::swapBackingFile(const QString &newFileName, const QUrl &url)
5028{
5029 if (!d->m_generator) {
5030 return false;
5031 }
5032
5033 if (!d->m_generator->hasFeature(Generator::SwapBackingFile)) {
5034 return false;
5035 }
5036
5037 // Save metadata about the file we're about to close
5038 d->saveDocumentInfo();
5039
5040 d->clearAndWaitForRequests();
5041
5042 qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName;
5043 QVector<Page *> newPagesVector;
5044 Generator::SwapBackingFileResult result = d->m_generator->swapBackingFile(newFileName, newPagesVector);
5045 if (result != Generator::SwapBackingFileError) {
5046 QList<ObjectRect *> rectsToDelete;
5047 QList<Annotation *> annotationsToDelete;
5048 QSet<PagePrivate *> pagePrivatesToDelete;
5049
5050 if (result == Generator::SwapBackingFileReloadInternalData) {
5051 // Here we need to replace everything that the old generator
5052 // had created with what the new one has without making it look like
5053 // we have actually closed and opened the file again
5054
5055 // Simple sanity check
5056 if (newPagesVector.count() != d->m_pagesVector.count()) {
5057 return false;
5058 }
5059
5060 // Update the undo stack contents
5061 for (int i = 0; i < d->m_undoStack->count(); ++i) {
5062 // Trust me on the const_cast ^_^
5063 QUndoCommand *uc = const_cast<QUndoCommand *>(d->m_undoStack->command(i));
5064 if (OkularUndoCommand *ouc = dynamic_cast<OkularUndoCommand *>(uc)) {
5065 const bool success = ouc->refreshInternalPageReferences(newPagesVector);
5066 if (!success) {
5067 qWarning() << "Document::swapBackingFile: refreshInternalPageReferences failed" << ouc;
5068 return false;
5069 }
5070 } else {
5071 qWarning() << "Document::swapBackingFile: Unhandled undo command" << uc;
5072 return false;
5073 }
5074 }
5075
5076 for (int i = 0; i < d->m_pagesVector.count(); ++i) {
5077 // switch the PagePrivate* from newPage to oldPage
5078 // this way everyone still holding Page* doesn't get
5079 // disturbed by it
5080 Page *oldPage = d->m_pagesVector[i];
5081 Page *newPage = newPagesVector[i];
5082 newPage->d->adoptGeneratedContents(oldPage->d);
5083
5084 pagePrivatesToDelete << oldPage->d;
5085 oldPage->d = newPage->d;
5086 oldPage->d->m_page = oldPage;
5087 oldPage->d->m_doc = d;
5088 newPage->d = nullptr;
5089
5090 annotationsToDelete << oldPage->m_annotations;
5091 rectsToDelete << oldPage->m_rects;
5092 oldPage->m_annotations = newPage->m_annotations;
5093 oldPage->m_rects = newPage->m_rects;
5094 }
5095 qDeleteAll(newPagesVector);
5096 }
5097
5098 d->m_url = url;
5099 d->m_docFileName = newFileName;
5100 d->updateMetadataXmlNameAndDocSize();
5101 d->m_bookmarkManager->setUrl(d->m_url);
5102 d->m_documentInfo = DocumentInfo();
5103 d->m_documentInfoAskedKeys.clear();
5104
5105 if (d->m_synctex_scanner) {
5106 synctex_scanner_free(d->m_synctex_scanner);
5107 d->m_synctex_scanner = synctex_scanner_new_with_output_file(QFile::encodeName(newFileName).constData(), nullptr, 1);
5108 if (!d->m_synctex_scanner && QFile::exists(newFileName + QLatin1String("sync"))) {
5109 d->loadSyncFile(newFileName);
5110 }
5111 }
5112
5113 foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::UrlChanged));
5114
5115 qDeleteAll(annotationsToDelete);
5116 qDeleteAll(rectsToDelete);
5117 qDeleteAll(pagePrivatesToDelete);
5118
5119 return true;
5120 } else {
5121 return false;
5122 }
5123}
5124
5125bool Document::swapBackingFileArchive(const QString &newFileName, const QUrl &url)
5126{
5127 qCDebug(OkularCoreDebug) << "Swapping backing archive to" << newFileName;
5128
5129 ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive(newFileName);
5130 if (!newArchive) {
5131 return false;
5132 }
5133
5134 const QString tempFileName = newArchive->document.fileName();
5135
5136 const bool success = swapBackingFile(tempFileName, url);
5137
5138 if (success) {
5139 delete d->m_archiveData;
5140 d->m_archiveData = newArchive;
5141 }
5142
5143 return success;
5144}
5145
5147{
5148 if (clean) {
5149 d->m_undoStack->setClean();
5150 } else {
5151 d->m_undoStack->resetClean();
5152 }
5153}
5154
5155bool Document::isHistoryClean() const
5156{
5157 return d->m_undoStack->isClean();
5158}
5159
5161{
5162 d->m_undoStack->clear();
5163}
5164
5166{
5167 if (!d->m_generator) {
5168 return false;
5169 }
5170 Q_ASSERT(!d->m_generatorName.isEmpty());
5171
5172 QHash<QString, GeneratorInfo>::iterator genIt = d->m_loadedGenerators.find(d->m_generatorName);
5173 Q_ASSERT(genIt != d->m_loadedGenerators.end());
5174 SaveInterface *saveIface = d->generatorSave(genIt.value());
5175 if (!saveIface) {
5176 return false;
5177 }
5178
5179 return saveIface->supportsOption(SaveInterface::SaveChanges);
5180}
5181
5183{
5184 switch (cap) {
5186 /* Assume that if the generator supports saving, forms can be saved.
5187 * We have no means to actually query the generator at the moment
5188 * TODO: Add some method to query the generator in SaveInterface */
5189 return canSaveChanges();
5190
5192 return d->canAddAnnotationsNatively();
5193 }
5194
5195 return false;
5196}
5197
5198bool Document::saveChanges(const QString &fileName)
5199{
5200 QString errorText;
5201 return saveChanges(fileName, &errorText);
5202}
5203
5204bool Document::saveChanges(const QString &fileName, QString *errorText)
5205{
5206 if (!d->m_generator || fileName.isEmpty()) {
5207 return false;
5208 }
5209 Q_ASSERT(!d->m_generatorName.isEmpty());
5210
5211 QHash<QString, GeneratorInfo>::iterator genIt = d->m_loadedGenerators.find(d->m_generatorName);
5212 Q_ASSERT(genIt != d->m_loadedGenerators.end());
5213 SaveInterface *saveIface = d->generatorSave(genIt.value());
5214 if (!saveIface || !saveIface->supportsOption(SaveInterface::SaveChanges)) {
5215 return false;
5216 }
5217
5218 if (const Okular::Action *action = d->m_generator->additionalDocumentAction(SaveDocumentStart)) {
5219 processDocumentAction(action, SaveDocumentStart);
5220 }
5221
5222 bool success = saveIface->save(fileName, SaveInterface::SaveChanges, errorText);
5223 if (success) {
5224 if (const Okular::Action *action = d->m_generator->additionalDocumentAction(SaveDocumentFinish)) {
5225 processDocumentAction(action, SaveDocumentFinish);
5226 }
5227 }
5228 return success;
5229}
5230
5232{
5233 if (!view) {
5234 return;
5235 }
5236
5237 Document *viewDoc = view->viewDocument();
5238 if (viewDoc) {
5239 // check if already registered for this document
5240 if (viewDoc == this) {
5241 return;
5242 }
5243
5244 viewDoc->unregisterView(view);
5245 }
5246
5247 d->m_views.insert(view);
5248 view->d_func()->document = d;
5249}
5250
5252{
5253 if (!view) {
5254 return;
5255 }
5256
5257 const Document *viewDoc = view->viewDocument();
5258 if (!viewDoc || viewDoc != this) {
5259 return;
5260 }
5261
5262 view->d_func()->document = nullptr;
5263 d->m_views.remove(view);
5264}
5265
5267{
5268 if (d->m_generator) {
5269 return d->m_generator->requestFontData(font);
5270 }
5271
5272 return {};
5273}
5274
5275ArchiveData *DocumentPrivate::unpackDocumentArchive(const QString &archivePath)
5276{
5277 QMimeDatabase db;
5278 const QMimeType mime = db.mimeTypeForFile(archivePath, QMimeDatabase::MatchExtension);
5279 if (!mime.inherits(QStringLiteral("application/vnd.kde.okular-archive"))) {
5280 return nullptr;
5281 }
5282
5283 KZip okularArchive(archivePath);
5284 if (!okularArchive.open(QIODevice::ReadOnly)) {
5285 return nullptr;
5286 }
5287
5288 const KArchiveDirectory *mainDir = okularArchive.directory();
5289
5290 // Check the archive doesn't have folders, we don't create them when saving the archive
5291 // and folders mean paths and paths mean path traversal issues
5292 const QStringList mainDirEntries = mainDir->entries();
5293 for (const QString &entry : mainDirEntries) {
5294 if (mainDir->entry(entry)->isDirectory()) {
5295 qWarning() << "Warning: Found a directory inside" << archivePath << " - Okular does not create files like that so it is most probably forged.";
5296 return nullptr;
5297 }
5298 }
5299
5300 const KArchiveEntry *mainEntry = mainDir->entry(QStringLiteral("content.xml"));
5301 if (!mainEntry || !mainEntry->isFile()) {
5302 return nullptr;
5303 }
5304
5305 std::unique_ptr<QIODevice> mainEntryDevice(static_cast<const KZipFileEntry *>(mainEntry)->createDevice());
5306 QDomDocument doc;
5307 if (!doc.setContent(mainEntryDevice.get())) {
5308 return nullptr;
5309 }
5310 mainEntryDevice.reset();
5311
5312 QDomElement root = doc.documentElement();
5313 if (root.tagName() != QLatin1String("OkularArchive")) {
5314 return nullptr;
5315 }
5316
5317 QString documentFileName;
5318 QString metadataFileName;
5319 QDomElement el = root.firstChild().toElement();
5320 for (; !el.isNull(); el = el.nextSibling().toElement()) {
5321 if (el.tagName() == QLatin1String("Files")) {
5322 QDomElement fileEl = el.firstChild().toElement();
5323 for (; !fileEl.isNull(); fileEl = fileEl.nextSibling().toElement()) {
5324 if (fileEl.tagName() == QLatin1String("DocumentFileName")) {
5325 documentFileName = fileEl.text();
5326 } else if (fileEl.tagName() == QLatin1String("MetadataFileName")) {
5327 metadataFileName = fileEl.text();
5328 }
5329 }
5330 }
5331 }
5332 if (documentFileName.isEmpty()) {
5333 return nullptr;
5334 }
5335
5336 const KArchiveEntry *docEntry = mainDir->entry(documentFileName);
5337 if (!docEntry || !docEntry->isFile()) {
5338 return nullptr;
5339 }
5340
5341 std::unique_ptr<ArchiveData> archiveData(new ArchiveData());
5342 const int dotPos = documentFileName.indexOf(QLatin1Char('.'));
5343 if (dotPos != -1) {
5344 archiveData->document.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX") + documentFileName.mid(dotPos));
5345 }
5346 if (!archiveData->document.open()) {
5347 return nullptr;
5348 }
5349
5350 archiveData->originalFileName = documentFileName;
5351
5352 {
5353 std::unique_ptr<QIODevice> docEntryDevice(static_cast<const KZipFileEntry *>(docEntry)->createDevice());
5354 copyQIODevice(docEntryDevice.get(), &archiveData->document);
5355 archiveData->document.close();
5356 }
5357
5358 const KArchiveEntry *metadataEntry = mainDir->entry(metadataFileName);
5359 if (metadataEntry && metadataEntry->isFile()) {
5360 std::unique_ptr<QIODevice> metadataEntryDevice(static_cast<const KZipFileEntry *>(metadataEntry)->createDevice());
5361 archiveData->metadataFile.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX.xml"));
5362 if (archiveData->metadataFile.open()) {
5363 copyQIODevice(metadataEntryDevice.get(), &archiveData->metadataFile);
5364 archiveData->metadataFile.close();
5365 }
5366 }
5367
5368 return archiveData.release();
5369}
5370
5371Document::OpenResult Document::openDocumentArchive(const QString &docFile, const QUrl &url, const QString &password)
5372{
5373 d->m_archiveData = DocumentPrivate::unpackDocumentArchive(docFile);
5374 if (!d->m_archiveData) {
5375 return OpenError;
5376 }
5377
5378 const QString tempFileName = d->m_archiveData->document.fileName();
5379 QMimeDatabase db;
5380 const QMimeType docMime = db.mimeTypeForFile(tempFileName, QMimeDatabase::MatchExtension);
5381 const OpenResult ret = openDocument(tempFileName, url, docMime, password);
5382
5383 if (ret != OpenSuccess) {
5384 delete d->m_archiveData;
5385 d->m_archiveData = nullptr;
5386 }
5387
5388 return ret;
5389}
5390
5392{
5393 if (!d->m_generator) {
5394 return false;
5395 }
5396
5397 /* If we opened an archive, use the name of original file (eg foo.pdf)
5398 * instead of the archive's one (eg foo.okular) */
5399 QString docFileName = d->m_archiveData ? d->m_archiveData->originalFileName : d->m_url.fileName();
5400 if (docFileName == QLatin1String("-")) {
5401 return false;