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 x %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 x %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 return false;
597 }
598
599 // Load DOM from XML file
600 QDomDocument doc(QStringLiteral("documentInfo"));
601 if (!doc.setContent(&infoFile)) {
602 qCDebug(OkularCoreDebug) << "Can't load XML pair! Check for broken xml.";
603 infoFile.close();
604 return false;
605 }
606 infoFile.close();
607
608 QDomElement root = doc.documentElement();
609
610 if (root.tagName() != QLatin1String("documentInfo")) {
611 return false;
612 }
613
614 bool loadedAnything = false; // set if something gets actually loaded
615
616 // Parse the DOM tree
617 QDomNode topLevelNode = root.firstChild();
618 while (topLevelNode.isElement()) {
619 QString catName = topLevelNode.toElement().tagName();
620
621 // Restore page attributes (bookmark, annotations, ...) from the DOM
622 if (catName == QLatin1String("pageList") && (loadWhat & LoadPageInfo)) {
623 QDomNode pageNode = topLevelNode.firstChild();
624 while (pageNode.isElement()) {
625 QDomElement pageElement = pageNode.toElement();
626 if (pageElement.hasAttribute(QStringLiteral("number"))) {
627 // get page number (node's attribute)
628 bool ok;
629 int pageNumber = pageElement.attribute(QStringLiteral("number")).toInt(&ok);
630
631 // pass the domElement to the right page, to read config data from
632 if (ok && pageNumber >= 0 && pageNumber < (int)m_pagesVector.count()) {
633 if (m_pagesVector[pageNumber]->d->restoreLocalContents(pageElement)) {
634 loadedAnything = true;
635 }
636 }
637 }
638 pageNode = pageNode.nextSibling();
639 }
640 }
641
642 // Restore 'general info' from the DOM
643 else if (catName == QLatin1String("generalInfo") && (loadWhat & LoadGeneralInfo)) {
644 QDomNode infoNode = topLevelNode.firstChild();
645 while (infoNode.isElement()) {
646 QDomElement infoElement = infoNode.toElement();
647
648 // restore viewports history
649 if (infoElement.tagName() == QLatin1String("history")) {
650 // clear history
651 m_viewportHistory.clear();
652 // append old viewports
653 QDomNode historyNode = infoNode.firstChild();
654 while (historyNode.isElement()) {
655 QDomElement historyElement = historyNode.toElement();
656 if (historyElement.hasAttribute(QStringLiteral("viewport"))) {
657 QString vpString = historyElement.attribute(QStringLiteral("viewport"));
658 m_viewportIterator = m_viewportHistory.insert(m_viewportHistory.end(), DocumentViewport(vpString));
659 loadedAnything = true;
660 }
661 historyNode = historyNode.nextSibling();
662 }
663 // consistency check
664 if (m_viewportHistory.empty()) {
665 m_viewportIterator = m_viewportHistory.insert(m_viewportHistory.end(), DocumentViewport());
666 }
667 } else if (infoElement.tagName() == QLatin1String("rotation")) {
668 QString str = infoElement.text();
669 bool ok = true;
670 int newrotation = !str.isEmpty() ? (str.toInt(&ok) % 4) : 0;
671 if (ok && newrotation != 0) {
672 setRotationInternal(newrotation, false);
673 loadedAnything = true;
674 }
675 } else if (infoElement.tagName() == QLatin1String("views")) {
676 QDomNode viewNode = infoNode.firstChild();
677 while (viewNode.isElement()) {
678 QDomElement viewElement = viewNode.toElement();
679 if (viewElement.tagName() == QLatin1String("view")) {
680 const QString viewName = viewElement.attribute(QStringLiteral("name"));
681 for (View *view : std::as_const(m_views)) {
682 if (view->name() == viewName) {
683 loadViewsInfo(view, viewElement);
684 loadedAnything = true;
685 break;
686 }
687 }
688 }
689 viewNode = viewNode.nextSibling();
690 }
691 }
692 infoNode = infoNode.nextSibling();
693 }
694 }
695
696 topLevelNode = topLevelNode.nextSibling();
697 } // </documentInfo>
698
699 return loadedAnything;
700}
701
702void DocumentPrivate::loadViewsInfo(View *view, const QDomElement &e)
703{
704 QDomNode viewNode = e.firstChild();
705 while (viewNode.isElement()) {
706 QDomElement viewElement = viewNode.toElement();
707
708 if (viewElement.tagName() == QLatin1String("zoom")) {
709 const QString valueString = viewElement.attribute(QStringLiteral("value"));
710 bool newzoom_ok = true;
711 const double newzoom = !valueString.isEmpty() ? valueString.toDouble(&newzoom_ok) : 1.0;
712 if (newzoom_ok && newzoom != 0 && view->supportsCapability(View::Zoom) && (view->capabilityFlags(View::Zoom) & (View::CapabilityRead | View::CapabilitySerializable))) {
713 view->setCapability(View::Zoom, newzoom);
714 }
715 const QString modeString = viewElement.attribute(QStringLiteral("mode"));
716 bool newmode_ok = true;
717 const int newmode = !modeString.isEmpty() ? modeString.toInt(&newmode_ok) : 2;
718 if (newmode_ok && view->supportsCapability(View::ZoomModality) && (view->capabilityFlags(View::ZoomModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
719 view->setCapability(View::ZoomModality, newmode);
720 }
721 } else if (viewElement.tagName() == QLatin1String("viewMode")) {
722 const QString modeString = viewElement.attribute(QStringLiteral("mode"));
723 bool newmode_ok = true;
724 const int newmode = !modeString.isEmpty() ? modeString.toInt(&newmode_ok) : 2;
725 if (newmode_ok && view->supportsCapability(View::ViewModeModality) && (view->capabilityFlags(View::ViewModeModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
726 view->setCapability(View::ViewModeModality, newmode);
727 }
728 } else if (viewElement.tagName() == QLatin1String("continuous")) {
729 const QString modeString = viewElement.attribute(QStringLiteral("mode"));
730 bool newmode_ok = true;
731 const int newmode = !modeString.isEmpty() ? modeString.toInt(&newmode_ok) : 2;
732 if (newmode_ok && view->supportsCapability(View::Continuous) && (view->capabilityFlags(View::Continuous) & (View::CapabilityRead | View::CapabilitySerializable))) {
733 view->setCapability(View::Continuous, newmode);
734 }
735 } else if (viewElement.tagName() == QLatin1String("trimMargins")) {
736 const QString valueString = viewElement.attribute(QStringLiteral("value"));
737 bool newmode_ok = true;
738 const int newmode = !valueString.isEmpty() ? valueString.toInt(&newmode_ok) : 2;
739 if (newmode_ok && view->supportsCapability(View::TrimMargins) && (view->capabilityFlags(View::TrimMargins) & (View::CapabilityRead | View::CapabilitySerializable))) {
740 view->setCapability(View::TrimMargins, newmode);
741 }
742 }
743
744 viewNode = viewNode.nextSibling();
745 }
746}
747
748void DocumentPrivate::saveViewsInfo(View *view, QDomElement &e) const
749{
750 if (view->supportsCapability(View::Zoom) && (view->capabilityFlags(View::Zoom) & (View::CapabilityRead | View::CapabilitySerializable)) && view->supportsCapability(View::ZoomModality) &&
751 (view->capabilityFlags(View::ZoomModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
752 QDomElement zoomEl = e.ownerDocument().createElement(QStringLiteral("zoom"));
753 e.appendChild(zoomEl);
754 bool ok = true;
755 const double zoom = view->capability(View::Zoom).toDouble(&ok);
756 if (ok && zoom != 0) {
757 zoomEl.setAttribute(QStringLiteral("value"), QString::number(zoom));
758 }
759 const int mode = view->capability(View::ZoomModality).toInt(&ok);
760 if (ok) {
761 zoomEl.setAttribute(QStringLiteral("mode"), mode);
762 }
763 }
764 if (view->supportsCapability(View::Continuous) && (view->capabilityFlags(View::Continuous) & (View::CapabilityRead | View::CapabilitySerializable))) {
765 QDomElement contEl = e.ownerDocument().createElement(QStringLiteral("continuous"));
766 e.appendChild(contEl);
767 const bool mode = view->capability(View::Continuous).toBool();
768 contEl.setAttribute(QStringLiteral("mode"), mode);
769 }
770 if (view->supportsCapability(View::ViewModeModality) && (view->capabilityFlags(View::ViewModeModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
771 QDomElement viewEl = e.ownerDocument().createElement(QStringLiteral("viewMode"));
772 e.appendChild(viewEl);
773 bool ok = true;
774 const int mode = view->capability(View::ViewModeModality).toInt(&ok);
775 if (ok) {
776 viewEl.setAttribute(QStringLiteral("mode"), mode);
777 }
778 }
779 if (view->supportsCapability(View::TrimMargins) && (view->capabilityFlags(View::TrimMargins) & (View::CapabilityRead | View::CapabilitySerializable))) {
780 QDomElement contEl = e.ownerDocument().createElement(QStringLiteral("trimMargins"));
781 e.appendChild(contEl);
782 const bool value = view->capability(View::TrimMargins).toBool();
783 contEl.setAttribute(QStringLiteral("value"), value);
784 }
785}
786
787QUrl DocumentPrivate::giveAbsoluteUrl(const QString &fileName) const
788{
789 if (!QDir::isRelativePath(fileName)) {
790 return QUrl::fromLocalFile(fileName);
791 }
792
793 if (!m_url.isValid()) {
794 return QUrl();
795 }
796
797 return QUrl(KIO::upUrl(m_url).toString() + fileName);
798}
799
800bool DocumentPrivate::openRelativeFile(const QString &fileName)
801{
802 const QUrl newUrl = giveAbsoluteUrl(fileName);
803 if (newUrl.isEmpty()) {
804 return false;
805 }
806
807 qCDebug(OkularCoreDebug).nospace() << "openRelativeFile: '" << newUrl << "'";
808
809 Q_EMIT m_parent->openUrl(newUrl);
810 return m_url == newUrl;
811}
812
813Generator *DocumentPrivate::loadGeneratorLibrary(const KPluginMetaData &service)
814{
815 const auto result = KPluginFactory::instantiatePlugin<Okular::Generator>(service);
816
817 if (!result) {
818 qCWarning(OkularCoreDebug).nospace() << "Failed to load plugin " << service.fileName() << ": " << result.errorText;
819 return nullptr;
820 }
821
822 GeneratorInfo info(result.plugin, service);
823 m_loadedGenerators.insert(service.pluginId(), info);
824 return result.plugin;
825}
826
827void DocumentPrivate::loadAllGeneratorLibraries()
828{
829 if (m_generatorsLoaded) {
830 return;
831 }
832
833 loadServiceList(availableGenerators());
834
835 m_generatorsLoaded = true;
836}
837
838void DocumentPrivate::loadServiceList(const QVector<KPluginMetaData> &offers)
839{
840 int count = offers.count();
841 if (count <= 0) {
842 return;
843 }
844
845 for (int i = 0; i < count; ++i) {
846 QString id = offers.at(i).pluginId();
847 // don't load already loaded generators
848 QHash<QString, GeneratorInfo>::const_iterator genIt = m_loadedGenerators.constFind(id);
849 if (!m_loadedGenerators.isEmpty() && genIt != m_loadedGenerators.constEnd()) {
850 continue;
851 }
852
853 Generator *g = loadGeneratorLibrary(offers.at(i));
854 (void)g;
855 }
856}
857
858void DocumentPrivate::unloadGenerator(const GeneratorInfo &info)
859{
860 delete info.generator;
861}
862
863void DocumentPrivate::cacheExportFormats()
864{
865 if (m_exportCached) {
866 return;
867 }
868
869 const ExportFormat::List formats = m_generator->exportFormats();
870 for (int i = 0; i < formats.count(); ++i) {
871 if (formats.at(i).mimeType().name() == QLatin1String("text/plain")) {
872 m_exportToText = formats.at(i);
873 } else {
874 m_exportFormats.append(formats.at(i));
875 }
876 }
877
878 m_exportCached = true;
879}
880
881ConfigInterface *DocumentPrivate::generatorConfig(GeneratorInfo &info)
882{
883 if (info.configChecked) {
884 return info.config;
885 }
886
887 info.config = qobject_cast<Okular::ConfigInterface *>(info.generator);
888 info.configChecked = true;
889 return info.config;
890}
891
892SaveInterface *DocumentPrivate::generatorSave(GeneratorInfo &info)
893{
894 if (info.saveChecked) {
895 return info.save;
896 }
897
898 info.save = qobject_cast<Okular::SaveInterface *>(info.generator);
899 info.saveChecked = true;
900 return info.save;
901}
902
903Document::OpenResult DocumentPrivate::openDocumentInternal(const KPluginMetaData &offer, bool isstdin, const QString &docFile, const QByteArray &filedata, const QString &password)
904{
905 QString propName = offer.pluginId();
906 QHash<QString, GeneratorInfo>::const_iterator genIt = m_loadedGenerators.constFind(propName);
907 m_walletGenerator = nullptr;
908 if (genIt != m_loadedGenerators.constEnd()) {
909 m_generator = genIt.value().generator;
910 } else {
911 m_generator = loadGeneratorLibrary(offer);
912 if (!m_generator) {
913 return Document::OpenError;
914 }
915 genIt = m_loadedGenerators.constFind(propName);
916 Q_ASSERT(genIt != m_loadedGenerators.constEnd());
917 }
918 Q_ASSERT_X(m_generator, "Document::load()", "null generator?!");
919
920 m_generator->d_func()->m_document = this;
921
922 // connect error reporting signals
923 m_openError.clear();
924 QMetaObject::Connection errorToOpenErrorConnection = QObject::connect(m_generator, &Generator::error, m_parent, [this](const QString &message) { m_openError = message; });
925 QObject::connect(m_generator, &Generator::warning, m_parent, &Document::warning);
926 QObject::connect(m_generator, &Generator::notice, m_parent, &Document::notice);
927
929
930 const QWindow *window = m_widget && m_widget->window() ? m_widget->window()->windowHandle() : nullptr;
931 const QSizeF dpi = Utils::realDpi(window);
932 qCDebug(OkularCoreDebug) << "Output DPI:" << dpi;
933 m_generator->setDPI(dpi);
934
935 Document::OpenResult openResult = Document::OpenError;
936 if (!isstdin) {
937 openResult = m_generator->loadDocumentWithPassword(docFile, m_pagesVector, password);
938 } else if (!filedata.isEmpty()) {
939 if (m_generator->hasFeature(Generator::ReadRawData)) {
940 openResult = m_generator->loadDocumentFromDataWithPassword(filedata, m_pagesVector, password);
941 } else {
942 m_tempFile = new QTemporaryFile();
943 if (!m_tempFile->open()) {
944 delete m_tempFile;
945 m_tempFile = nullptr;
946 } else {
947 m_tempFile->write(filedata);
948 QString tmpFileName = m_tempFile->fileName();
949 m_tempFile->close();
950 openResult = m_generator->loadDocumentWithPassword(tmpFileName, m_pagesVector, password);
951 }
952 }
953 }
954
956 if (openResult != Document::OpenSuccess || m_pagesVector.size() <= 0) {
957 m_generator->d_func()->m_document = nullptr;
958 QObject::disconnect(m_generator, nullptr, m_parent, nullptr);
959
960 // TODO this is a bit of a hack, since basically means that
961 // you can only call walletDataForFile after calling openDocument
962 // but since in reality it's what happens I've decided not to refactor/break API
963 // One solution is just kill walletDataForFile and make OpenResult be an object
964 // where the wallet data is also returned when OpenNeedsPassword
965 m_walletGenerator = m_generator;
966 m_generator = nullptr;
967
968 qDeleteAll(m_pagesVector);
969 m_pagesVector.clear();
970 delete m_tempFile;
971 m_tempFile = nullptr;
972
973 // TODO: Q_EMIT a message telling the document is empty
974 if (openResult == Document::OpenSuccess) {
975 openResult = Document::OpenError;
976 }
977 } else {
978 /*
979 * Now that the documen is opened, the tab (if using tabs) is visible, which mean that
980 * we can now connect the error reporting signal directly to the parent
981 */
982
983 QObject::disconnect(errorToOpenErrorConnection);
984 QObject::connect(m_generator, &Generator::error, m_parent, &Document::error);
985 }
986
987 return openResult;
988}
989
990bool DocumentPrivate::savePageDocumentInfo(QTemporaryFile *infoFile, int what) const
991{
992 if (infoFile->open()) {
993 // 1. Create DOM
994 QDomDocument doc(QStringLiteral("documentInfo"));
995 QDomProcessingInstruction xmlPi = doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
996 doc.appendChild(xmlPi);
997 QDomElement root = doc.createElement(QStringLiteral("documentInfo"));
998 doc.appendChild(root);
999
1000 // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM
1001 QDomElement pageList = doc.createElement(QStringLiteral("pageList"));
1002 root.appendChild(pageList);
1003 // <page list><page number='x'>.... </page> save pages that hold data
1004 QVector<Page *>::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd();
1005 for (; pIt != pEnd; ++pIt) {
1006 (*pIt)->d->saveLocalContents(pageList, doc, PageItems(what));
1007 }
1008
1009 // 3. Save DOM to XML file
1010 QString xml = doc.toString();
1011
1012 QTextStream os(infoFile);
1013 os.setEncoding(QStringConverter::Utf8);
1014 os << xml;
1015 return true;
1016 }
1017 return false;
1018}
1019
1020DocumentViewport DocumentPrivate::nextDocumentViewport() const
1021{
1022 DocumentViewport ret = m_nextDocumentViewport;
1023 if (!m_nextDocumentDestination.isEmpty() && m_generator) {
1024 DocumentViewport vp(m_parent->metaData(QStringLiteral("NamedViewport"), m_nextDocumentDestination).toString());
1025 if (vp.isValid()) {
1026 ret = vp;
1027 }
1028 }
1029 return ret;
1030}
1031
1032void DocumentPrivate::performAddPageAnnotation(int page, Annotation *annotation)
1033{
1034 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
1035 AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr;
1036
1037 // find out the page to attach annotation
1038 Page *kp = m_pagesVector[page];
1039 if (!m_generator || !kp) {
1040 return;
1041 }
1042
1043 // the annotation belongs already to a page
1044 if (annotation->d_ptr->m_page) {
1045 return;
1046 }
1047
1048 // add annotation to the page
1049 kp->addAnnotation(annotation);
1050
1051 // tell the annotation proxy
1052 if (proxy && proxy->supports(AnnotationProxy::Addition)) {
1053 proxy->notifyAddition(annotation, page);
1054 }
1055
1056 // notify observers about the change
1057 notifyAnnotationChanges(page);
1058
1059 if (annotation->flags() & Annotation::ExternallyDrawn) {
1060 // Redraw everything, including ExternallyDrawn annotations
1061 refreshPixmaps(page);
1062 }
1063}
1064
1065void DocumentPrivate::performRemovePageAnnotation(int page, Annotation *annotation)
1066{
1067 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
1068 AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr;
1069 bool isExternallyDrawn;
1070
1071 // find out the page
1072 Page *kp = m_pagesVector[page];
1073 if (!m_generator || !kp) {
1074 return;
1075 }
1076
1077 if (annotation->flags() & Annotation::ExternallyDrawn) {
1078 isExternallyDrawn = true;
1079 } else {
1080 isExternallyDrawn = false;
1081 }
1082
1083 // try to remove the annotation
1084 if (m_parent->canRemovePageAnnotation(annotation)) {
1085 // tell the annotation proxy
1086 if (proxy && proxy->supports(AnnotationProxy::Removal)) {
1087 proxy->notifyRemoval(annotation, page);
1088 }
1089
1090 kp->removeAnnotation(annotation); // Also destroys the object
1091
1092 // in case of success, notify observers about the change
1093 notifyAnnotationChanges(page);
1094
1095 if (isExternallyDrawn) {
1096 // Redraw everything, including ExternallyDrawn annotations
1097 refreshPixmaps(page);
1098 }
1099 }
1100}
1101
1102void DocumentPrivate::performModifyPageAnnotation(int page, Annotation *annotation, bool appearanceChanged)
1103{
1104 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
1105 AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr;
1106
1107 // find out the page
1108 const Page *kp = m_pagesVector[page];
1109 if (!m_generator || !kp) {
1110 return;
1111 }
1112
1113 // tell the annotation proxy
1114 if (proxy && proxy->supports(AnnotationProxy::Modification)) {
1115 proxy->notifyModification(annotation, page, appearanceChanged);
1116 }
1117
1118 // notify observers about the change
1119 notifyAnnotationChanges(page);
1120 if (appearanceChanged && (annotation->flags() & Annotation::ExternallyDrawn)) {
1121 /* When an annotation is being moved, the generator will not render it.
1122 * Therefore there's no need to refresh pixmaps after the first time */
1123 if (annotation->flags() & (Annotation::BeingMoved | Annotation::BeingResized)) {
1124 if (m_annotationBeingModified) {
1125 return;
1126 } else { // First time: take note
1127 m_annotationBeingModified = true;
1128 }
1129 } else {
1130 m_annotationBeingModified = false;
1131 }
1132
1133 // Redraw everything, including ExternallyDrawn annotations
1134 qCDebug(OkularCoreDebug) << "Refreshing Pixmaps";
1135 refreshPixmaps(page);
1136 }
1137}
1138
1139void DocumentPrivate::performSetAnnotationContents(const QString &newContents, Annotation *annot, int pageNumber)
1140{
1141 bool appearanceChanged = false;
1142
1143 // Check if appearanceChanged should be true
1144 switch (annot->subType()) {
1145 // If it's an in-place TextAnnotation, set the inplace text
1147 const Okular::TextAnnotation *txtann = static_cast<Okular::TextAnnotation *>(annot);
1148 if (txtann->textType() == Okular::TextAnnotation::InPlace) {
1149 appearanceChanged = true;
1150 }
1151 break;
1152 }
1153 // If it's a LineAnnotation, check if caption text is visible
1155 const Okular::LineAnnotation *lineann = static_cast<Okular::LineAnnotation *>(annot);
1156 if (lineann->showCaption()) {
1157 appearanceChanged = true;
1158 }
1159 break;
1160 }
1161 default:
1162 break;
1163 }
1164
1165 // Set contents
1166 annot->setContents(newContents);
1167
1168 // Tell the document the annotation has been modified
1169 performModifyPageAnnotation(pageNumber, annot, appearanceChanged);
1170}
1171
1172void DocumentPrivate::recalculateForms()
1173{
1174 const QVariant fco = m_parent->metaData(QStringLiteral("FormCalculateOrder"));
1175 const QVector<int> formCalculateOrder = fco.value<QVector<int>>();
1176 for (int formId : formCalculateOrder) {
1177 for (uint pageIdx = 0; pageIdx < m_parent->pages(); pageIdx++) {
1178 const Page *p = m_parent->page(pageIdx);
1179 if (p) {
1180 bool pageNeedsRefresh = false;
1181 const QList<Okular::FormField *> forms = p->formFields();
1182 for (FormField *form : forms) {
1183 if (form->id() == formId) {
1184 const Action *action = form->additionalAction(FormField::CalculateField);
1185 if (action) {
1186 FormFieldText *fft = dynamic_cast<FormFieldText *>(form);
1187 std::shared_ptr<Event> event;
1188 QString oldVal;
1189 if (fft) {
1190 // Prepare text calculate event
1191 event = Event::createFormCalculateEvent(fft, m_pagesVector[pageIdx]);
1192 if (!m_scripter) {
1193 m_scripter = new Scripter(this);
1194 }
1195 m_scripter->setEvent(event.get());
1196 // The value maybe changed in javascript so save it first.
1197 oldVal = fft->text();
1198 }
1199
1200 m_parent->processAction(action);
1201 if (event && fft) {
1202 // Update text field from calculate
1203 m_scripter->setEvent(nullptr);
1204 const QString newVal = event->value().toString();
1205 if (newVal != oldVal) {
1206 fft->setText(newVal);
1207 fft->setAppearanceText(newVal);
1209 // The format action handles the refresh.
1210 m_parent->processFormatAction(action, form);
1211 } else {
1212 Q_EMIT m_parent->refreshFormWidget(fft);
1213 pageNeedsRefresh = true;
1214 }
1215 }
1216 }
1217 } else {
1218 qWarning() << "Form that is part of calculate order doesn't have a calculate action";
1219 }
1220 }
1221 }
1222 if (pageNeedsRefresh) {
1223 refreshPixmaps(p->number());
1224 }
1225 }
1226 }
1227 }
1228}
1229
1230void DocumentPrivate::saveDocumentInfo() const
1231{
1232 if (m_xmlFileName.isEmpty()) {
1233 return;
1234 }
1235
1236 QFile infoFile(m_xmlFileName);
1237 qCDebug(OkularCoreDebug) << "About to save document info to" << m_xmlFileName;
1239 qCWarning(OkularCoreDebug) << "Failed to open docdata file" << m_xmlFileName;
1240 return;
1241 }
1242 // 1. Create DOM
1243 QDomDocument doc(QStringLiteral("documentInfo"));
1244 QDomProcessingInstruction xmlPi = doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
1245 doc.appendChild(xmlPi);
1246 QDomElement root = doc.createElement(QStringLiteral("documentInfo"));
1247 root.setAttribute(QStringLiteral("url"), m_url.toDisplayString(QUrl::PreferLocalFile));
1248 doc.appendChild(root);
1249
1250 // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM
1251 // -> do this if there are not-yet-migrated annots or forms in docdata/
1252 if (m_docdataMigrationNeeded) {
1253 QDomElement pageList = doc.createElement(QStringLiteral("pageList"));
1254 root.appendChild(pageList);
1255 // OriginalAnnotationPageItems and OriginalFormFieldPageItems tell to
1256 // store the same unmodified annotation list and form contents that we
1257 // read when we opened the file and ignore any change made by the user.
1258 // Since we don't store annotations and forms in docdata/ any more, this is
1259 // necessary to preserve annotations/forms that previous Okular version
1260 // had stored there.
1261 const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems | OriginalFormFieldPageItems;
1262 // <page list><page number='x'>.... </page> save pages that hold data
1263 QVector<Page *>::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd();
1264 for (; pIt != pEnd; ++pIt) {
1265 (*pIt)->d->saveLocalContents(pageList, doc, saveWhat);
1266 }
1267 }
1268
1269 // 2.2. Save document info (current viewport, history, ... ) to DOM
1270 QDomElement generalInfo = doc.createElement(QStringLiteral("generalInfo"));
1271 root.appendChild(generalInfo);
1272 // create rotation node
1273 if (m_rotation != Rotation0) {
1274 QDomElement rotationNode = doc.createElement(QStringLiteral("rotation"));
1275 generalInfo.appendChild(rotationNode);
1276 rotationNode.appendChild(doc.createTextNode(QString::number((int)m_rotation)));
1277 }
1278 // <general info><history> ... </history> save history up to OKULAR_HISTORY_SAVEDSTEPS viewports
1279 const auto currentViewportIterator = std::list<DocumentViewport>::const_iterator(m_viewportIterator);
1280 std::list<DocumentViewport>::const_iterator backIterator = currentViewportIterator;
1281 if (backIterator != m_viewportHistory.end()) {
1282 // go back up to OKULAR_HISTORY_SAVEDSTEPS steps from the current viewportIterator
1283 int backSteps = OKULAR_HISTORY_SAVEDSTEPS;
1284 while (backSteps-- && backIterator != m_viewportHistory.begin()) {
1285 --backIterator;
1286 }
1287
1288 // create history root node
1289 QDomElement historyNode = doc.createElement(QStringLiteral("history"));
1290 generalInfo.appendChild(historyNode);
1291
1292 // add old[backIterator] and present[viewportIterator] items
1293 std::list<DocumentViewport>::const_iterator endIt = currentViewportIterator;
1294 ++endIt;
1295 while (backIterator != endIt) {
1296 QString name = (backIterator == currentViewportIterator) ? QStringLiteral("current") : QStringLiteral("oldPage");
1297 QDomElement historyEntry = doc.createElement(name);
1298 historyEntry.setAttribute(QStringLiteral("viewport"), (*backIterator).toString());
1299 historyNode.appendChild(historyEntry);
1300 ++backIterator;
1301 }
1302 }
1303 // create views root node
1304 QDomElement viewsNode = doc.createElement(QStringLiteral("views"));
1305 generalInfo.appendChild(viewsNode);
1306 for (View *view : std::as_const(m_views)) {
1307 QDomElement viewEntry = doc.createElement(QStringLiteral("view"));
1308 viewEntry.setAttribute(QStringLiteral("name"), view->name());
1309 viewsNode.appendChild(viewEntry);
1310 saveViewsInfo(view, viewEntry);
1311 }
1312
1313 // 3. Save DOM to XML file
1314 QString xml = doc.toString();
1315 QTextStream os(&infoFile);
1316 os.setEncoding(QStringConverter::Utf8);
1317 os << xml;
1318 infoFile.close();
1319}
1320
1321void DocumentPrivate::slotTimedMemoryCheck()
1322{
1323 // [MEM] clean memory (for 'free mem dependent' profiles only)
1324 if (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Low && m_allocatedPixmapsTotalMemory > 1024 * 1024) {
1325 cleanupPixmapMemory();
1326 }
1327}
1328
1329void DocumentPrivate::sendGeneratorPixmapRequest()
1330{
1331 /* If the pixmap cache will have to be cleaned in order to make room for the
1332 * next request, get the distance from the current viewport of the page
1333 * whose pixmap will be removed. We will ignore preload requests for pages
1334 * that are at the same distance or farther */
1335 const qulonglong memoryToFree = calculateMemoryToFree();
1336 const int currentViewportPage = (*m_viewportIterator).pageNumber;
1337 int maxDistance = INT_MAX; // Default: No maximum
1338 if (memoryToFree) {
1339 const AllocatedPixmap *pixmapToReplace = searchLowestPriorityPixmap(true);
1340 if (pixmapToReplace) {
1341 maxDistance = qAbs(pixmapToReplace->page - currentViewportPage);
1342 }
1343 }
1344
1345 // find a request
1346 PixmapRequest *request = nullptr;
1347 m_pixmapRequestsMutex.lock();
1348 while (!m_pixmapRequestsStack.empty() && !request) {
1349 PixmapRequest *r = m_pixmapRequestsStack.back();
1350 if (!r) {
1351 m_pixmapRequestsStack.pop_back();
1352 continue;
1353 }
1354
1355 QRect requestRect = r->isTile() ? r->normalizedRect().geometry(r->width(), r->height()) : QRect(0, 0, r->width(), r->height());
1356 TilesManager *tilesManager = r->d->tilesManager();
1357 const double normalizedArea = r->normalizedRect().width() * r->normalizedRect().height();
1358 const QScreen *screen = nullptr;
1359 if (m_widget) {
1360 const QWindow *window = m_widget->window()->windowHandle();
1361 if (window) {
1362 screen = window->screen();
1363 }
1364 }
1365 if (!screen) {
1367 }
1368 const long screenSize = screen->devicePixelRatio() * screen->size().width() * screen->devicePixelRatio() * screen->size().height();
1369
1370 // Make sure the page is the right size to receive the pixmap
1371 r->page()->setPageSize(r->observer(), r->width(), r->height());
1372
1373 // If it's a preload but the generator is not threaded no point in trying to preload
1374 if (r->preload() && !m_generator->hasFeature(Generator::Threaded)) {
1375 m_pixmapRequestsStack.pop_back();
1376 delete r;
1377 }
1378 // request only if page isn't already present and request has valid id
1379 else if ((!r->d->mForce && r->page()->hasPixmap(r->observer(), r->width(), r->height(), r->normalizedRect())) || !m_observers.contains(r->observer())) {
1380 m_pixmapRequestsStack.pop_back();
1381 delete r;
1382 } else if (!r->d->mForce && r->preload() && qAbs(r->pageNumber() - currentViewportPage) >= maxDistance) {
1383 m_pixmapRequestsStack.pop_back();
1384 // qCDebug(OkularCoreDebug) << "Ignoring request that doesn't fit in cache";
1385 delete r;
1386 }
1387 // Ignore requests for pixmaps that are already being generated
1388 else if (tilesManager && tilesManager->isRequesting(r->normalizedRect(), r->width(), r->height())) {
1389 m_pixmapRequestsStack.pop_back();
1390 delete r;
1391 }
1392 // If the requested area is above 4*screenSize pixels, and we're not rendering most of the page, switch on the tile manager
1393 else if (!tilesManager && m_generator->hasFeature(Generator::TiledRendering) && (long)r->width() * (long)r->height() > 4L * screenSize && normalizedArea < 0.75) {
1394 // if the image is too big. start using tiles
1395 qCDebug(OkularCoreDebug).nospace() << "Start using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1396
1397 // fill the tiles manager with the last rendered pixmap
1398 const QPixmap *pixmap = r->page()->_o_nearestPixmap(r->observer(), r->width(), r->height());
1399 if (pixmap) {
1400 tilesManager = new TilesManager(r->pageNumber(), pixmap->width(), pixmap->height(), r->page()->rotation());
1401 tilesManager->setPixmap(pixmap, NormalizedRect(0, 0, 1, 1), true /*isPartialPixmap*/);
1402 tilesManager->setSize(r->width(), r->height());
1403 } else {
1404 // create new tiles manager
1405 tilesManager = new TilesManager(r->pageNumber(), r->width(), r->height(), r->page()->rotation());
1406 }
1407 tilesManager->setRequest(r->normalizedRect(), r->width(), r->height());
1408 r->page()->deletePixmap(r->observer());
1409 r->page()->d->setTilesManager(r->observer(), tilesManager);
1410 r->setTile(true);
1411
1412 // Change normalizedRect to the smallest rect that contains all
1413 // visible tiles.
1414 if (!r->normalizedRect().isNull()) {
1415 NormalizedRect tilesRect;
1416 const QList<Tile> tiles = tilesManager->tilesAt(r->normalizedRect(), TilesManager::TerminalTile);
1417 QList<Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
1418 while (tIt != tEnd) {
1419 Tile tile = *tIt;
1420 if (tilesRect.isNull()) {
1421 tilesRect = tile.rect();
1422 } else {
1423 tilesRect |= tile.rect();
1424 }
1425
1426 ++tIt;
1427 }
1428
1429 r->setNormalizedRect(tilesRect);
1430 request = r;
1431 } else {
1432 // Discard request if normalizedRect is null. This happens in
1433 // preload requests issued by PageView if the requested page is
1434 // not visible and the user has just switched from a non-tiled
1435 // zoom level to a tiled one
1436 m_pixmapRequestsStack.pop_back();
1437 delete r;
1438 }
1439 }
1440 // If the requested area is below 3*screenSize pixels, switch off the tile manager
1441 else if (tilesManager && (long)r->width() * (long)r->height() < 3L * screenSize) {
1442 qCDebug(OkularCoreDebug).nospace() << "Stop using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1443
1444 // page is too small. stop using tiles.
1445 r->page()->deletePixmap(r->observer());
1446 r->setTile(false);
1447
1448 request = r;
1449 } else if ((long)requestRect.width() * (long)requestRect.height() > 100L * screenSize && (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Greedy)) {
1450 m_pixmapRequestsStack.pop_back();
1451 if (!m_warnedOutOfMemory) {
1452 qCWarning(OkularCoreDebug).nospace() << "Running out of memory on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1453 qCWarning(OkularCoreDebug) << "this message will be reported only once.";
1454 m_warnedOutOfMemory = true;
1455 }
1456 delete r;
1457 } else {
1458 request = r;
1459 }
1460 }
1461
1462 // if no request found (or already generated), return
1463 if (!request) {
1464 m_pixmapRequestsMutex.unlock();
1465 return;
1466 }
1467
1468 // [MEM] preventive memory freeing
1469 qulonglong pixmapBytes = 0;
1470 TilesManager *tm = request->d->tilesManager();
1471 if (tm) {
1472 pixmapBytes = tm->totalMemory();
1473 } else {
1474 pixmapBytes = 4 * request->width() * request->height();
1475 }
1476
1477 if (pixmapBytes > (1024 * 1024)) {
1478 cleanupPixmapMemory(memoryToFree /* previously calculated value */);
1479 }
1480
1481 // submit the request to the generator
1482 if (m_generator->canGeneratePixmap()) {
1483 QRect requestRect = !request->isTile() ? QRect(0, 0, request->width(), request->height()) : request->normalizedRect().geometry(request->width(), request->height());
1484 qCDebug(OkularCoreDebug).nospace() << "sending request observer=" << request->observer() << " " << requestRect.width() << "x" << requestRect.height() << "@" << request->pageNumber() << " async == " << request->asynchronous()
1485 << " isTile == " << request->isTile();
1486 m_pixmapRequestsStack.remove(request);
1487
1488 if (tm) {
1489 tm->setRequest(request->normalizedRect(), request->width(), request->height());
1490 }
1491
1492 if ((int)m_rotation % 2) {
1493 request->d->swap();
1494 }
1495
1496 if (m_rotation != Rotation0 && !request->normalizedRect().isNull()) {
1497 request->setNormalizedRect(TilesManager::fromRotatedRect(request->normalizedRect(), m_rotation));
1498 }
1499
1500 // If set elsewhere we already know we want it to be partial
1501 if (!request->partialUpdatesWanted()) {
1502 request->setPartialUpdatesWanted(request->asynchronous() && !request->page()->hasPixmap(request->observer()));
1503 }
1504
1505 // we always have to unlock _before_ the generatePixmap() because
1506 // a sync generation would end with requestDone() -> deadlock, and
1507 // we can not really know if the generator can do async requests
1508 m_executingPixmapRequests.push_back(request);
1509 m_pixmapRequestsMutex.unlock();
1510 m_generator->generatePixmap(request);
1511 } else {
1512 m_pixmapRequestsMutex.unlock();
1513 // pino (7/4/2006): set the polling interval from 10 to 30
1514 QTimer::singleShot(30, m_parent, [this] { sendGeneratorPixmapRequest(); });
1515 }
1516}
1517
1518void DocumentPrivate::rotationFinished(int page, Okular::Page *okularPage)
1519{
1520 const Okular::Page *wantedPage = m_pagesVector.value(page, nullptr);
1521 if (!wantedPage || wantedPage != okularPage) {
1522 return;
1523 }
1524
1525 for (DocumentObserver *o : std::as_const(m_observers)) {
1526 o->notifyPageChanged(page, DocumentObserver::Pixmap | DocumentObserver::Annotations);
1527 }
1528}
1529
1530void DocumentPrivate::slotFontReadingProgress(int page)
1531{
1532 Q_EMIT m_parent->fontReadingProgress(page);
1533
1534 if (page >= (int)m_parent->pages() - 1) {
1535 Q_EMIT m_parent->fontReadingEnded();
1536 m_fontThread = nullptr;
1537 m_fontsCached = true;
1538 }
1539}
1540
1541void DocumentPrivate::fontReadingGotFont(const Okular::FontInfo &font)
1542{
1543 // Try to avoid duplicate fonts
1544 if (m_fontsCache.indexOf(font) == -1) {
1545 m_fontsCache.append(font);
1546
1547 Q_EMIT m_parent->gotFont(font);
1548 }
1549}
1550
1551void DocumentPrivate::slotGeneratorConfigChanged()
1552{
1553 if (!m_generator) {
1554 return;
1555 }
1556
1557 // reparse generator config and if something changed clear Pages
1558 bool configchanged = false;
1559 QHash<QString, GeneratorInfo>::iterator it = m_loadedGenerators.begin(), itEnd = m_loadedGenerators.end();
1560 for (; it != itEnd; ++it) {
1561 Okular::ConfigInterface *iface = generatorConfig(it.value());
1562 if (iface) {
1563 bool it_changed = iface->reparseConfig();
1564 if (it_changed && (m_generator == it.value().generator)) {
1565 configchanged = true;
1566 }
1567 }
1568 }
1569 if (configchanged) {
1570 // invalidate pixmaps
1571 QVector<Page *>::const_iterator it = m_pagesVector.constBegin(), end = m_pagesVector.constEnd();
1572 for (; it != end; ++it) {
1573 (*it)->deletePixmaps();
1574 }
1575
1576 // [MEM] remove allocation descriptors
1577 qDeleteAll(m_allocatedPixmaps);
1578 m_allocatedPixmaps.clear();
1579 m_allocatedPixmapsTotalMemory = 0;
1580
1581 // send reload signals to observers
1582 foreachObserverD(notifyContentsCleared(DocumentObserver::Pixmap));
1583 }
1584
1585 // free memory if in 'low' profile
1586 if (SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !m_allocatedPixmaps.empty() && !m_pagesVector.isEmpty()) {
1587 cleanupPixmapMemory();
1588 }
1589}
1590
1591void DocumentPrivate::refreshPixmaps(int pageNumber)
1592{
1593 Page *page = m_pagesVector.value(pageNumber, nullptr);
1594 if (!page) {
1595 return;
1596 }
1597
1598 QMap<DocumentObserver *, PagePrivate::PixmapObject>::ConstIterator it = page->d->m_pixmaps.constBegin(), itEnd = page->d->m_pixmaps.constEnd();
1599 QVector<Okular::PixmapRequest *> pixmapsToRequest;
1600 for (; it != itEnd; ++it) {
1601 const QSize size = (*it).m_pixmap->size();
1602 PixmapRequest *p = new PixmapRequest(it.key(), pageNumber, size.width(), size.height(), 1 /* dpr */, 1, PixmapRequest::Asynchronous);
1603 p->d->mForce = true;
1604 pixmapsToRequest << p;
1605 }
1606
1607 // Need to do this ↑↓ in two steps since requestPixmaps can end up calling cancelRenderingBecauseOf
1608 // which changes m_pixmaps and thus breaks the loop above
1609 for (PixmapRequest *pr : std::as_const(pixmapsToRequest)) {
1610 QList<Okular::PixmapRequest *> requestedPixmaps;
1611 requestedPixmaps.push_back(pr);
1612 m_parent->requestPixmaps(requestedPixmaps, Okular::Document::NoOption);
1613 }
1614
1615 for (DocumentObserver *observer : std::as_const(m_observers)) {
1616 QList<Okular::PixmapRequest *> requestedPixmaps;
1617
1618 TilesManager *tilesManager = page->d->tilesManager(observer);
1619 if (tilesManager) {
1620 tilesManager->markDirty();
1621
1622 PixmapRequest *p = new PixmapRequest(observer, pageNumber, tilesManager->width(), tilesManager->height(), 1 /* dpr */, 1, PixmapRequest::Asynchronous);
1623
1624 // Get the visible page rect
1625 NormalizedRect visibleRect;
1626 QVector<Okular::VisiblePageRect *>::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd();
1627 for (; vIt != vEnd; ++vIt) {
1628 if ((*vIt)->pageNumber == pageNumber) {
1629 visibleRect = (*vIt)->rect;
1630 break;
1631 }
1632 }
1633
1634 if (!visibleRect.isNull()) {
1635 p->setNormalizedRect(visibleRect);
1636 p->setTile(true);
1637 p->d->mForce = true;
1638 requestedPixmaps.push_back(p);
1639 } else {
1640 delete p;
1641 }
1642 }
1643
1644 m_parent->requestPixmaps(requestedPixmaps, Okular::Document::NoOption);
1645 }
1646}
1647
1648void DocumentPrivate::_o_configChanged()
1649{
1650 // free text pages if needed
1651 calculateMaxTextPages();
1652 while (m_allocatedTextPagesFifo.count() > m_maxAllocatedTextPages) {
1653 int pageToKick = m_allocatedTextPagesFifo.takeFirst();
1654 m_pagesVector.at(pageToKick)->setTextPage(nullptr); // deletes the textpage
1655 }
1656}
1657
1658void DocumentPrivate::doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct)
1659{
1660 DoContinueDirectionMatchSearchStruct *searchStruct = static_cast<DoContinueDirectionMatchSearchStruct *>(doContinueDirectionMatchSearchStruct);
1661 RunningSearch *search = m_searches.value(searchStruct->searchID);
1662
1663 if ((m_searchCancelled && !searchStruct->match) || !search) {
1664 // if the user cancelled but he just got a match, give him the match!
1666
1667 if (search) {
1668 search->isCurrentlySearching = false;
1669 }
1670
1671 Q_EMIT m_parent->searchFinished(searchStruct->searchID, Document::SearchCancelled);
1672 delete searchStruct->pagesToNotify;
1673 delete searchStruct;
1674 return;
1675 }
1676
1677 const bool forward = search->cachedType == Document::NextMatch;
1678 bool doContinue = false;
1679 // if no match found, loop through the whole doc, starting from currentPage
1680 if (!searchStruct->match) {
1681 const int pageCount = m_pagesVector.count();
1682 if (search->pagesDone < pageCount) {
1683 doContinue = true;
1684 if (searchStruct->currentPage >= pageCount) {
1685 searchStruct->currentPage = 0;
1686 Q_EMIT m_parent->notice(i18n("Continuing search from beginning"), 3000);
1687 } else if (searchStruct->currentPage < 0) {
1688 searchStruct->currentPage = pageCount - 1;
1689 Q_EMIT m_parent->notice(i18n("Continuing search from bottom"), 3000);
1690 }
1691 }
1692 }
1693
1694 if (doContinue) {
1695 // get page
1696 const Page *page = m_pagesVector[searchStruct->currentPage];
1697 // request search page if needed
1698 if (!page->hasTextPage()) {
1699 m_parent->requestTextPage(page->number());
1700 }
1701
1702 // if found a match on the current page, end the loop
1703 searchStruct->match = page->findText(searchStruct->searchID, search->cachedString, forward ? FromTop : FromBottom, search->cachedCaseSensitivity);
1704 if (!searchStruct->match) {
1705 if (forward) {
1706 searchStruct->currentPage++;
1707 } else {
1708 searchStruct->currentPage--;
1709 }
1710 search->pagesDone++;
1711 } else {
1712 search->pagesDone = 1;
1713 }
1714
1715 // Both of the previous if branches need to call doContinueDirectionMatchSearch
1716 QTimer::singleShot(0, m_parent, [this, searchStruct] { doContinueDirectionMatchSearch(searchStruct); });
1717 } else {
1718 doProcessSearchMatch(searchStruct->match, search, searchStruct->pagesToNotify, searchStruct->currentPage, searchStruct->searchID, search->cachedViewportMove, search->cachedColor);
1719 delete searchStruct;
1720 }
1721}
1722
1723void DocumentPrivate::doProcessSearchMatch(RegularAreaRect *match, RunningSearch *search, QSet<int> *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor &color)
1724{
1725 // reset cursor to previous shape
1727
1728 bool foundAMatch = false;
1729
1730 search->isCurrentlySearching = false;
1731
1732 // if a match has been found..
1733 if (match) {
1734 // update the RunningSearch structure adding this match..
1735 foundAMatch = true;
1736 search->continueOnPage = currentPage;
1737 search->continueOnMatch = *match;
1738 search->highlightedPages.insert(currentPage);
1739 // ..add highlight to the page..
1740 m_pagesVector[currentPage]->d->setHighlight(searchID, match, color);
1741
1742 // ..queue page for notifying changes..
1743 pagesToNotify->insert(currentPage);
1744
1745 // Create a normalized rectangle around the search match that includes a 5% buffer on all sides.
1746 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);
1747
1748 const bool matchRectFullyVisible = isNormalizedRectangleFullyVisible(matchRectWithBuffer, currentPage);
1749
1750 // ..move the viewport to show the first of the searched word sequence centered
1751 if (moveViewport && !matchRectFullyVisible) {
1752 DocumentViewport searchViewport(currentPage);
1753 searchViewport.rePos.enabled = true;
1754 searchViewport.rePos.normalizedX = (match->first().left + match->first().right) / 2.0;
1755 searchViewport.rePos.normalizedY = (match->first().top + match->first().bottom) / 2.0;
1756 m_parent->setViewport(searchViewport, nullptr, true);
1757 }
1758 delete match;
1759 }
1760
1761 // notify observers about highlights changes
1762 for (int pageNumber : std::as_const(*pagesToNotify)) {
1763 for (DocumentObserver *observer : std::as_const(m_observers)) {
1764 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1765 }
1766 }
1767
1768 if (foundAMatch) {
1769 Q_EMIT m_parent->searchFinished(searchID, Document::MatchFound);
1770 } else {
1771 Q_EMIT m_parent->searchFinished(searchID, Document::NoMatchFound);
1772 }
1773
1774 delete pagesToNotify;
1775}
1776
1777void DocumentPrivate::doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID)
1778{
1779 QMap<Page *, QVector<RegularAreaRect *>> *pageMatches = static_cast<QMap<Page *, QVector<RegularAreaRect *>> *>(pageMatchesMap);
1780 QSet<int> *pagesToNotify = static_cast<QSet<int> *>(pagesToNotifySet);
1781 RunningSearch *search = m_searches.value(searchID);
1782
1783 if (m_searchCancelled || !search) {
1784 typedef QVector<RegularAreaRect *> MatchesVector;
1785
1787
1788 if (search) {
1789 search->isCurrentlySearching = false;
1790 }
1791
1792 Q_EMIT m_parent->searchFinished(searchID, Document::SearchCancelled);
1793 for (const MatchesVector &mv : std::as_const(*pageMatches)) {
1794 qDeleteAll(mv);
1795 }
1796 delete pageMatches;
1797 delete pagesToNotify;
1798 return;
1799 }
1800
1801 if (currentPage < m_pagesVector.count()) {
1802 // get page (from the first to the last)
1803 Page *page = m_pagesVector.at(currentPage);
1804 int pageNumber = page->number(); // redundant? is it == currentPage ?
1805
1806 // request search page if needed
1807 if (!page->hasTextPage()) {
1808 m_parent->requestTextPage(pageNumber);
1809 }
1810
1811 // loop on a page adding highlights for all found items
1812 RegularAreaRect *lastMatch = nullptr;
1813 while (true) {
1814 if (lastMatch) {
1815 lastMatch = page->findText(searchID, search->cachedString, NextResult, search->cachedCaseSensitivity, lastMatch);
1816 } else {
1817 lastMatch = page->findText(searchID, search->cachedString, FromTop, search->cachedCaseSensitivity);
1818 }
1819
1820 if (!lastMatch) {
1821 break;
1822 }
1823
1824 // add highlight rect to the matches map
1825 (*pageMatches)[page].append(lastMatch);
1826 }
1827 delete lastMatch;
1828
1829 QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID] { doContinueAllDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID); });
1830 } else {
1831 // reset cursor to previous shape
1833
1834 search->isCurrentlySearching = false;
1835 bool foundAMatch = pageMatches->count() != 0;
1836 QMap<Page *, QVector<RegularAreaRect *>>::const_iterator it, itEnd;
1837 it = pageMatches->constBegin();
1838 itEnd = pageMatches->constEnd();
1839 for (; it != itEnd; ++it) {
1840 for (RegularAreaRect *match : it.value()) {
1841 it.key()->d->setHighlight(searchID, match, search->cachedColor);
1842 delete match;
1843 }
1844 search->highlightedPages.insert(it.key()->number());
1845 pagesToNotify->insert(it.key()->number());
1846 }
1847
1848 for (DocumentObserver *observer : std::as_const(m_observers)) {
1849 observer->notifySetup(m_pagesVector, 0);
1850 }
1851
1852 // notify observers about highlights changes
1853 for (int pageNumber : std::as_const(*pagesToNotify)) {
1854 for (DocumentObserver *observer : std::as_const(m_observers)) {
1855 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1856 }
1857 }
1858
1859 if (foundAMatch) {
1860 Q_EMIT m_parent->searchFinished(searchID, Document::MatchFound);
1861 } else {
1862 Q_EMIT m_parent->searchFinished(searchID, Document::NoMatchFound);
1863 }
1864
1865 delete pageMatches;
1866 delete pagesToNotify;
1867 }
1868}
1869
1870void DocumentPrivate::doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList &words)
1871{
1872 typedef QPair<RegularAreaRect *, QColor> MatchColor;
1873 QMap<Page *, QVector<MatchColor>> *pageMatches = static_cast<QMap<Page *, QVector<MatchColor>> *>(pageMatchesMap);
1874 QSet<int> *pagesToNotify = static_cast<QSet<int> *>(pagesToNotifySet);
1875 RunningSearch *search = m_searches.value(searchID);
1876
1877 if (m_searchCancelled || !search) {
1878 typedef QVector<MatchColor> MatchesVector;
1879
1881
1882 if (search) {
1883 search->isCurrentlySearching = false;
1884 }
1885
1886 Q_EMIT m_parent->searchFinished(searchID, Document::SearchCancelled);
1887
1888 for (const MatchesVector &mv : std::as_const(*pageMatches)) {
1889 for (const MatchColor &mc : mv) {
1890 delete mc.first;
1891 }
1892 }
1893 delete pageMatches;
1894 delete pagesToNotify;
1895 return;
1896 }
1897
1898 const int wordCount = words.count();
1899 const int hueStep = (wordCount > 1) ? (60 / (wordCount - 1)) : 60;
1900 int baseHue, baseSat, baseVal;
1901 search->cachedColor.getHsv(&baseHue, &baseSat, &baseVal);
1902
1903 if (currentPage < m_pagesVector.count()) {
1904 // get page (from the first to the last)
1905 Page *page = m_pagesVector.at(currentPage);
1906 int pageNumber = page->number(); // redundant? is it == currentPage ?
1907
1908 // request search page if needed
1909 if (!page->hasTextPage()) {
1910 m_parent->requestTextPage(pageNumber);
1911 }
1912
1913 // loop on a page adding highlights for all found items
1914 bool allMatched = wordCount > 0, anyMatched = false;
1915 for (int w = 0; w < wordCount; w++) {
1916 const QString &word = words[w];
1917 int newHue = baseHue - w * hueStep;
1918 if (newHue < 0) {
1919 newHue += 360;
1920 }
1921 QColor wordColor = QColor::fromHsv(newHue, baseSat, baseVal);
1922 RegularAreaRect *lastMatch = nullptr;
1923 // add all highlights for current word
1924 bool wordMatched = false;
1925 while (true) {
1926 if (lastMatch) {
1927 lastMatch = page->findText(searchID, word, NextResult, search->cachedCaseSensitivity, lastMatch);
1928 } else {
1929 lastMatch = page->findText(searchID, word, FromTop, search->cachedCaseSensitivity);
1930 }
1931
1932 if (!lastMatch) {
1933 break;
1934 }
1935
1936 // add highligh rect to the matches map
1937 (*pageMatches)[page].append(MatchColor(lastMatch, wordColor));
1938 wordMatched = true;
1939 }
1940 allMatched = allMatched && wordMatched;
1941 anyMatched = anyMatched || wordMatched;
1942 }
1943
1944 // if not all words are present in page, remove partial highlights
1945 const bool matchAll = search->cachedType == Document::GoogleAll;
1946 if (!allMatched && matchAll) {
1947 const QVector<MatchColor> &matches = (*pageMatches)[page];
1948 for (const MatchColor &mc : matches) {
1949 delete mc.first;
1950 }
1951 pageMatches->remove(page);
1952 }
1953
1954 QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID, words] { doContinueGooglesDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID, words); });
1955 } else {
1956 // reset cursor to previous shape
1958
1959 search->isCurrentlySearching = false;
1960 bool foundAMatch = pageMatches->count() != 0;
1961 QMap<Page *, QVector<MatchColor>>::const_iterator it, itEnd;
1962 it = pageMatches->constBegin();
1963 itEnd = pageMatches->constEnd();
1964 for (; it != itEnd; ++it) {
1965 for (const MatchColor &mc : it.value()) {
1966 it.key()->d->setHighlight(searchID, mc.first, mc.second);
1967 delete mc.first;
1968 }
1969 search->highlightedPages.insert(it.key()->number());
1970 pagesToNotify->insert(it.key()->number());
1971 }
1972
1973 // send page lists to update observers (since some filter on bookmarks)
1974 for (DocumentObserver *observer : std::as_const(m_observers)) {
1975 observer->notifySetup(m_pagesVector, 0);
1976 }
1977
1978 // notify observers about highlights changes
1979 for (int pageNumber : std::as_const(*pagesToNotify)) {
1980 for (DocumentObserver *observer : std::as_const(m_observers)) {
1981 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1982 }
1983 }
1984
1985 if (foundAMatch) {
1986 Q_EMIT m_parent->searchFinished(searchID, Document::MatchFound);
1987 } else {
1988 Q_EMIT m_parent->searchFinished(searchID, Document::NoMatchFound);
1989 }
1990
1991 delete pageMatches;
1992 delete pagesToNotify;
1993 }
1994}
1995
1996QVariant DocumentPrivate::documentMetaData(const Generator::DocumentMetaDataKey key, const QVariant &option) const
1997{
1998 switch (key) {
2000 bool giveDefault = option.toBool();
2001 QColor color;
2002 if ((SettingsCore::renderMode() == SettingsCore::EnumRenderMode::Paper) && SettingsCore::changeColors()) {
2003 color = SettingsCore::paperColor();
2004 } else if (giveDefault) {
2005 color = Qt::white;
2006 }
2007 return color;
2008 } break;
2009
2011 switch (SettingsCore::textAntialias()) {
2012 case SettingsCore::EnumTextAntialias::Enabled:
2013 return true;
2014 break;
2015 case SettingsCore::EnumTextAntialias::Disabled:
2016 return false;
2017 break;
2018 }
2019 break;
2020
2022 switch (SettingsCore::graphicsAntialias()) {
2023 case SettingsCore::EnumGraphicsAntialias::Enabled:
2024 return true;
2025 break;
2026 case SettingsCore::EnumGraphicsAntialias::Disabled:
2027 return false;
2028 break;
2029 }
2030 break;
2031
2033 switch (SettingsCore::textHinting()) {
2034 case SettingsCore::EnumTextHinting::Enabled:
2035 return true;
2036 break;
2037 case SettingsCore::EnumTextHinting::Disabled:
2038 return false;
2039 break;
2040 }
2041 break;
2042 }
2043 return QVariant();
2044}
2045
2046bool DocumentPrivate::isNormalizedRectangleFullyVisible(const Okular::NormalizedRect &rectOfInterest, int rectPage)
2047{
2048 bool rectFullyVisible = false;
2049 const QVector<Okular::VisiblePageRect *> &visibleRects = m_parent->visiblePageRects();
2052
2053 for (; (vIt != vEnd) && !rectFullyVisible; ++vIt) {
2054 if ((*vIt)->pageNumber == rectPage && (*vIt)->rect.contains(rectOfInterest.left, rectOfInterest.top) && (*vIt)->rect.contains(rectOfInterest.right, rectOfInterest.bottom)) {
2055 rectFullyVisible = true;
2056 }
2057 }
2058 return rectFullyVisible;
2059}
2060
2061struct pdfsyncpoint {
2062 QString file;
2063 qlonglong x;
2064 qlonglong y;
2065 int row;
2066 int column;
2067 int page;
2068};
2069
2070void DocumentPrivate::loadSyncFile(const QString &filePath)
2071{
2072 QFile f(filePath + QLatin1String("sync"));
2073 if (!f.open(QIODevice::ReadOnly)) {
2074 return;
2075 }
2076
2077 QTextStream ts(&f);
2078 // first row: core name of the pdf output
2079 const QString coreName = ts.readLine();
2080 // second row: version string, in the form 'Version %u'
2081 const QString versionstr = ts.readLine();
2082 // anchor the pattern with \A and \z to match the entire subject string
2083 // TODO: with Qt 5.12 QRegularExpression::anchoredPattern() can be used instead
2084 static QRegularExpression versionre(QStringLiteral("\\AVersion \\d+\\z"), QRegularExpression::CaseInsensitiveOption);
2085 QRegularExpressionMatch match = versionre.match(versionstr);
2086 if (!match.hasMatch()) {
2087 return;
2088 }
2089
2091 QStack<QString> fileStack;
2092 int currentpage = -1;
2093 const QLatin1String texStr(".tex");
2094 const QChar spaceChar = QChar::fromLatin1(' ');
2095
2096 fileStack.push(coreName + texStr);
2097
2098 const QSizeF dpi = m_generator->dpi();
2099
2100 QString line;
2101 while (!ts.atEnd()) {
2102 line = ts.readLine();
2103 const QStringList tokens = line.split(spaceChar, Qt::SkipEmptyParts);
2104 const int tokenSize = tokens.count();
2105 if (tokenSize < 1) {
2106 continue;
2107 }
2108 if (tokens.first() == QLatin1String("l") && tokenSize >= 3) {
2109 int id = tokens.at(1).toInt();
2111 if (it == points.constEnd()) {
2112 pdfsyncpoint pt;
2113 pt.x = 0;
2114 pt.y = 0;
2115 pt.row = tokens.at(2).toInt();
2116 pt.column = 0; // TODO
2117 pt.page = -1;
2118 pt.file = fileStack.top();
2119 points[id] = pt;
2120 }
2121 } else if (tokens.first() == QLatin1String("s") && tokenSize >= 2) {
2122 currentpage = tokens.at(1).toInt() - 1;
2123 } else if (tokens.first() == QLatin1String("p*") && tokenSize >= 4) {
2124 // TODO
2125 qCDebug(OkularCoreDebug) << "PdfSync: 'p*' line ignored";
2126 } else if (tokens.first() == QLatin1String("p") && tokenSize >= 4) {
2127 int id = tokens.at(1).toInt();
2129 if (it != points.end()) {
2130 it->x = tokens.at(2).toInt();
2131 it->y = tokens.at(3).toInt();
2132 it->page = currentpage;
2133 }
2134 } else if (line.startsWith(QLatin1Char('(')) && tokenSize == 1) {
2135 QString newfile = line;
2136 // chop the leading '('
2137 newfile.remove(0, 1);
2138 if (!newfile.endsWith(texStr)) {
2139 newfile += texStr;
2140 }
2141 fileStack.push(newfile);
2142 } else if (line == QLatin1String(")")) {
2143 if (!fileStack.isEmpty()) {
2144 fileStack.pop();
2145 } else {
2146 qCDebug(OkularCoreDebug) << "PdfSync: going one level down too much";
2147 }
2148 } else {
2149 qCDebug(OkularCoreDebug).nospace() << "PdfSync: unknown line format: '" << line << "'";
2150 }
2151 }
2152
2153 QVector<QList<Okular::SourceRefObjectRect *>> refRects(m_pagesVector.size());
2154 for (const pdfsyncpoint &pt : std::as_const(points)) {
2155 // drop pdfsync points not completely valid
2156 if (pt.page < 0 || pt.page >= m_pagesVector.size()) {
2157 continue;
2158 }
2159
2160 // magic numbers for TeX's RSU's (Ridiculously Small Units) conversion to pixels
2161 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()));
2162 QString file = pt.file;
2163 Okular::SourceReference *sourceRef = new Okular::SourceReference(file, pt.row, pt.column);
2164 refRects[pt.page].append(new Okular::SourceRefObjectRect(p, sourceRef));
2165 }
2166 for (int i = 0; i < refRects.size(); ++i) {
2167 if (!refRects.at(i).isEmpty()) {
2168 m_pagesVector[i]->setSourceReferences(refRects.at(i));
2169 }
2170 }
2171}
2172
2173void DocumentPrivate::clearAndWaitForRequests()
2174{
2175 m_pixmapRequestsMutex.lock();
2176 std::list<PixmapRequest *>::const_iterator sIt = m_pixmapRequestsStack.begin();
2177 std::list<PixmapRequest *>::const_iterator sEnd = m_pixmapRequestsStack.end();
2178 for (; sIt != sEnd; ++sIt) {
2179 delete *sIt;
2180 }
2181 m_pixmapRequestsStack.clear();
2182 m_pixmapRequestsMutex.unlock();
2183
2184 QEventLoop loop;
2185 bool startEventLoop = false;
2186 do {
2187 m_pixmapRequestsMutex.lock();
2188 startEventLoop = !m_executingPixmapRequests.empty();
2189
2190 if (m_generator->hasFeature(Generator::SupportsCancelling)) {
2191 for (PixmapRequest *executingRequest : std::as_const(m_executingPixmapRequests)) {
2192 executingRequest->d->mShouldAbortRender = 1;
2193 }
2194
2195 if (m_generator->d_ptr->mTextPageGenerationThread) {
2196 m_generator->d_ptr->mTextPageGenerationThread->abortExtraction();
2197 }
2198 }
2199
2200 m_pixmapRequestsMutex.unlock();
2201 if (startEventLoop) {
2202 m_closingLoop = &loop;
2203 loop.exec();
2204 m_closingLoop = nullptr;
2205 }
2206 } while (startEventLoop);
2207}
2208
2209int DocumentPrivate::findFieldPageNumber(Okular::FormField *field)
2210{
2211 // Lookup the page of the FormField
2212 int foundPage = -1;
2213 for (uint pageIdx = 0, nPages = m_parent->pages(); pageIdx < nPages; pageIdx++) {
2214 const Page *p = m_parent->page(pageIdx);
2215 if (p && p->formFields().contains(field)) {
2216 foundPage = static_cast<int>(pageIdx);
2217 break;
2218 }
2219 }
2220 return foundPage;
2221}
2222
2223void DocumentPrivate::executeScriptEvent(const std::shared_ptr<Event> &event, const Okular::ScriptAction *linkscript)
2224{
2225 if (!m_scripter) {
2226 m_scripter = new Scripter(this);
2227 }
2228 m_scripter->setEvent(event.get());
2229 m_scripter->execute(linkscript->scriptType(), linkscript->script());
2230
2231 // Clear out the event after execution
2232 m_scripter->setEvent(nullptr);
2233}
2234
2235Document::Document(QWidget *widget)
2236 : QObject(nullptr)
2237 , d(new DocumentPrivate(this))
2238{
2239 d->m_widget = widget;
2240 d->m_bookmarkManager = new BookmarkManager(d);
2241 d->m_viewportIterator = d->m_viewportHistory.insert(d->m_viewportHistory.end(), DocumentViewport());
2242 d->m_undoStack = new QUndoStack(this);
2243
2244 connect(SettingsCore::self(), &SettingsCore::configChanged, this, [this] { d->_o_configChanged(); });
2248
2249 qRegisterMetaType<Okular::FontInfo>();
2250}
2251
2253{
2254 // delete generator, pages, and related stuff
2255 closeDocument();
2256
2257 QSet<View *>::const_iterator viewIt = d->m_views.constBegin(), viewEnd = d->m_views.constEnd();
2258 for (; viewIt != viewEnd; ++viewIt) {
2259 View *v = *viewIt;
2260 v->d_func()->document = nullptr;
2261 }
2262
2263 // delete the bookmark manager
2264 delete d->m_bookmarkManager;
2265
2266 // delete the loaded generators
2267 QHash<QString, GeneratorInfo>::const_iterator it = d->m_loadedGenerators.constBegin(), itEnd = d->m_loadedGenerators.constEnd();
2268 for (; it != itEnd; ++it) {
2269 d->unloadGenerator(it.value());
2270 }
2271 d->m_loadedGenerators.clear();
2272
2273 // delete the private structure
2274 delete d;
2275}
2276
2277QString DocumentPrivate::docDataFileName(const QUrl &url, qint64 document_size)
2278{
2279 QString fn = url.fileName();
2280 fn = QString::number(document_size) + QLatin1Char('.') + fn + QStringLiteral(".xml");
2281 QString docdataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/okular/docdata");
2282 // make sure that the okular/docdata/ directory exists (probably this used to be handled by KStandardDirs)
2283 if (!QFileInfo::exists(docdataDir)) {
2284 qCDebug(OkularCoreDebug) << "creating docdata folder" << docdataDir;
2285 QDir().mkpath(docdataDir);
2286 }
2287 QString newokularfile = docdataDir + QLatin1Char('/') + fn;
2288
2289 return newokularfile;
2290}
2291
2292QVector<KPluginMetaData> DocumentPrivate::availableGenerators()
2293{
2294 static QVector<KPluginMetaData> result;
2295 if (result.isEmpty()) {
2296 result = KPluginMetaData::findPlugins(QStringLiteral("okular_generators"));
2297 }
2298 return result;
2299}
2300
2301KPluginMetaData DocumentPrivate::generatorForMimeType(const QMimeType &type, QWidget *widget, const QVector<KPluginMetaData> &triedOffers)
2302{
2303 // First try to find an exact match, and then look for more general ones (e. g. the plain text one)
2304 // Ideally we would rank these by "closeness", but that might be overdoing it
2305
2306 const QVector<KPluginMetaData> available = availableGenerators();
2308 QVector<KPluginMetaData> exactMatches;
2309
2310 QMimeDatabase mimeDatabase;
2311
2312 for (const KPluginMetaData &md : available) {
2313 if (triedOffers.contains(md)) {
2314 continue;
2315 }
2316
2317 const QStringList mimetypes = md.mimeTypes();
2318 for (const QString &supported : mimetypes) {
2319 QMimeType mimeType = mimeDatabase.mimeTypeForName(supported);
2320 if (mimeType == type && !exactMatches.contains(md)) {
2321 exactMatches << md;
2322 }
2323
2324 if (type.inherits(supported) && !offers.contains(md)) {
2325 offers << md;
2326 }
2327 }
2328 }
2329
2330 if (!exactMatches.isEmpty()) {
2331 offers = exactMatches;
2332 }
2333
2334 if (offers.isEmpty()) {
2335 return KPluginMetaData();
2336 }
2337 int hRank = 0;
2338 // best ranked offer search
2339 int offercount = offers.size();
2340 if (offercount > 1) {
2341 // sort the offers: the offers with an higher priority come before
2342 auto cmp = [](const KPluginMetaData &s1, const KPluginMetaData &s2) {
2343 const QString property = QStringLiteral("X-KDE-Priority");
2344 return s1.rawData().value(property).toInt() > s2.rawData().value(property).toInt();
2345 };
2346 std::stable_sort(offers.begin(), offers.end(), cmp);
2347
2348 if (SettingsCore::chooseGenerators()) {
2350 for (int i = 0; i < offercount; ++i) {
2351 list << offers.at(i).pluginId();
2352 }
2353 ChooseEngineDialog choose(list, type, widget);
2354
2355 if (choose.exec() == QDialog::Rejected) {
2356 return KPluginMetaData();
2357 }
2358
2359 hRank = choose.selectedGenerator();
2360 }
2361 }
2362 Q_ASSERT(hRank < offers.size());
2363 return offers.at(hRank);
2364}
2365
2366Document::OpenResult Document::openDocument(const QString &docFile, const QUrl &url, const QMimeType &_mime, const QString &password)
2367{
2368 QMimeDatabase db;
2369 QMimeType mime = _mime;
2370 QByteArray filedata;
2371 int fd = -1;
2372 if (url.scheme() == QLatin1String("fd")) {
2373 bool ok;
2374 fd = QStringView {url.path()}.mid(1).toInt(&ok);
2375 if (!ok) {
2376 return OpenError;
2377 }
2378 } else if (url.fileName() == QLatin1String("-")) {
2379 fd = 0;
2380 }
2381 bool triedMimeFromFileContent = false;
2382 if (fd < 0) {
2383 if (!mime.isValid()) {
2384 return OpenError;
2385 }
2386
2387 d->m_url = url;
2388 d->m_docFileName = docFile;
2389
2390 if (!d->updateMetadataXmlNameAndDocSize()) {
2391 return OpenError;
2392 }
2393 } else {
2394 QFile qstdin;
2395 const bool ret = qstdin.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle);
2396 if (!ret) {
2397 qWarning() << "failed to read" << url << filedata;
2398 return OpenError;
2399 }
2400
2401 filedata = qstdin.readAll();
2402 mime = db.mimeTypeForData(filedata);
2403 if (!mime.isValid() || mime.isDefault()) {
2404 return OpenError;
2405 }
2406 d->m_docSize = filedata.size();
2407 triedMimeFromFileContent = true;
2408 }
2409
2410 const bool fromFileDescriptor = fd >= 0;
2411
2412 // 0. load Generator
2413 // request only valid non-disabled plugins suitable for the mimetype
2414 KPluginMetaData offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2415 if (!offer.isValid() && !triedMimeFromFileContent) {
2417 triedMimeFromFileContent = true;
2418 if (newmime != mime) {
2419 mime = newmime;
2420 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2421 }
2422 if (!offer.isValid()) {
2423 // There's still no offers, do a final mime search based on the filename
2424 // We need this because sometimes (e.g. when downloading from a webserver) the mimetype we
2425 // use is the one fed by the server, that may be wrong
2426 newmime = db.mimeTypeForUrl(url);
2427
2428 if (!newmime.isDefault() && newmime != mime) {
2429 mime = newmime;
2430 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2431 }
2432 }
2433 }
2434 if (!offer.isValid()) {
2435 d->m_openError = i18n("Can not find a plugin which is able to handle the document being passed.");
2436 Q_EMIT error(d->m_openError, -1);
2437 qCWarning(OkularCoreDebug).nospace() << "No plugin for mimetype '" << mime.name() << "'.";
2438 return OpenError;
2439 }
2440
2441 // 1. load Document
2442 OpenResult openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2443 if (openResult == OpenError) {
2444 QVector<KPluginMetaData> triedOffers;
2445 triedOffers << offer;
2446 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2447
2448 while (offer.isValid()) {
2449 openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2450
2451 if (openResult == OpenError) {
2452 triedOffers << offer;
2453 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2454 } else {
2455 break;
2456 }
2457 }
2458
2459 if (openResult == OpenError && !triedMimeFromFileContent) {
2461 triedMimeFromFileContent = true;
2462 if (newmime != mime) {
2463 mime = newmime;
2464 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2465 while (offer.isValid()) {
2466 openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2467
2468 if (openResult == OpenError) {
2469 triedOffers << offer;
2470 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2471 } else {
2472 break;
2473 }
2474 }
2475 }
2476 }
2477
2478 if (openResult == OpenSuccess) {
2479 // Clear errors, since we're trying various generators, maybe one of them errored out
2480 // but we finally succeeded
2481 // TODO one can still see the error message animating out but since this is a very rare
2482 // condition we can leave this for future work
2483 Q_EMIT error(QString(), -1);
2484 }
2485 }
2486 if (openResult != OpenSuccess) {
2487 return openResult;
2488 }
2489
2490 // no need to check for the existence of a synctex file, no parser will be
2491 // created if none exists
2492 d->m_synctex_scanner = synctex_scanner_new_with_output_file(QFile::encodeName(docFile).constData(), nullptr, 1);
2493 if (!d->m_synctex_scanner && QFile::exists(docFile + QLatin1String("sync"))) {
2494 d->loadSyncFile(docFile);
2495 }
2496
2497 d->m_generatorName = offer.pluginId();
2498 d->m_pageController = new PageController();
2499 connect(d->m_pageController, &PageController::rotationFinished, this, [this](int p, Okular::Page *op) { d->rotationFinished(p, op); });
2500
2501 for (Page *p : std::as_const(d->m_pagesVector)) {
2502 p->d->m_doc = d;
2503 }
2504
2505 d->m_docdataMigrationNeeded = false;
2506
2507 // 2. load Additional Data (bookmarks, local annotations and metadata) about the document
2508 if (d->m_archiveData) {
2509 // QTemporaryFile is weird and will return false in exists if fileName wasn't called before
2510 d->m_archiveData->metadataFile.fileName();
2511 d->loadDocumentInfo(d->m_archiveData->metadataFile, LoadPageInfo);
2512 d->loadDocumentInfo(LoadGeneralInfo);
2513 } else {
2514 if (d->loadDocumentInfo(LoadPageInfo)) {
2515 d->m_docdataMigrationNeeded = true;
2516 }
2517 d->loadDocumentInfo(LoadGeneralInfo);
2518 }
2519
2520 d->m_bookmarkManager->setUrl(d->m_url);
2521
2522 // 3. setup observers internal lists and data
2523 foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged));
2524
2525 // 4. set initial page (restoring the page saved in xml if loaded)
2526 DocumentViewport loadedViewport = (*d->m_viewportIterator);
2527 if (loadedViewport.isValid()) {
2528 (*d->m_viewportIterator) = DocumentViewport();
2529 if (loadedViewport.pageNumber >= (int)d->m_pagesVector.size()) {
2530 loadedViewport.pageNumber = d->m_pagesVector.size() - 1;
2531 }
2532 } else {
2533 loadedViewport.pageNumber = 0;
2534 }
2535 setViewport(loadedViewport);
2536
2537 // start bookmark saver timer
2538 if (!d->m_saveBookmarksTimer) {
2539 d->m_saveBookmarksTimer = new QTimer(this);
2540 connect(d->m_saveBookmarksTimer, &QTimer::timeout, this, [this] { d->saveDocumentInfo(); });
2541 }
2542 d->m_saveBookmarksTimer->start(5 * 60 * 1000);
2543
2544 // start memory check timer
2545 if (!d->m_memCheckTimer) {
2546 d->m_memCheckTimer = new QTimer(this);
2547 connect(d->m_memCheckTimer, &QTimer::timeout, this, [this] { d->slotTimedMemoryCheck(); });
2548 }
2549 d->m_memCheckTimer->start(kMemCheckTime);
2550
2551 const DocumentViewport nextViewport = d->nextDocumentViewport();
2552 if (nextViewport.isValid()) {
2553 setViewport(nextViewport);
2554 d->m_nextDocumentViewport = DocumentViewport();
2555 d->m_nextDocumentDestination = QString();
2556 }
2557
2558 AudioPlayer::instance()->setDocument(fromFileDescriptor ? QUrl() : d->m_url, this);
2559
2560 const QStringList docScripts = d->m_generator->metaData(QStringLiteral("DocumentScripts"), QStringLiteral("JavaScript")).toStringList();
2561 if (!docScripts.isEmpty()) {
2562 d->m_scripter = new Scripter(d);
2563 for (const QString &docscript : docScripts) {
2564 const Okular::ScriptAction *linkScript = new Okular::ScriptAction(Okular::JavaScript, docscript);
2565 std::shared_ptr<Event> event = Event::createDocEvent(Event::DocOpen);
2566 d->executeScriptEvent(event, linkScript);
2567 }
2568 }
2569
2570 return OpenSuccess;
2571}
2572
2573bool DocumentPrivate::updateMetadataXmlNameAndDocSize()
2574{
2575 // m_docFileName is always local so we can use QFileInfo on it
2576 QFileInfo fileReadTest(m_docFileName);
2577 if (!fileReadTest.isFile() && !fileReadTest.isReadable()) {
2578 return false;
2579 }
2580
2581 m_docSize = fileReadTest.size();
2582
2583 // determine the related "xml document-info" filename
2584 if (m_url.isLocalFile()) {
2585 const QString filePath = docDataFileName(m_url, m_docSize);
2586 qCDebug(OkularCoreDebug) << "Metadata file is now:" << filePath;
2587 m_xmlFileName = filePath;
2588 } else {
2589 qCDebug(OkularCoreDebug) << "Metadata file: disabled";
2590 m_xmlFileName = QString();
2591 }
2592
2593 return true;
2594}
2595
2597{
2598 if (d->m_generator) {
2600 if (iface) {
2601 return iface->guiClient();
2602 }
2603 }
2604 return nullptr;
2605}
2606
2608{
2609 // check if there's anything to close...
2610 if (!d->m_generator) {
2611 return;
2612 }
2613
2614 if (const Okular::Action *action = d->m_generator->additionalDocumentAction(CloseDocument)) {
2615 processDocumentAction(action, CloseDocument);
2616 }
2617
2619
2620 delete d->m_pageController;
2621 d->m_pageController = nullptr;
2622
2623 delete d->m_scripter;
2624 d->m_scripter = nullptr;
2625
2626 // remove requests left in queue
2627 d->clearAndWaitForRequests();
2628
2629 if (d->m_fontThread) {
2630 disconnect(d->m_fontThread, nullptr, this, nullptr);
2631 d->m_fontThread->stopExtraction();
2632 d->m_fontThread->wait();
2633 d->m_fontThread = nullptr;
2634 }
2635
2636 // stop any audio playback
2638
2639 // close the current document and save document info if a document is still opened
2640 if (d->m_generator && d->m_pagesVector.size() > 0) {
2641 d->saveDocumentInfo();
2642
2643 // free the content of the opaque backend actions (if any)
2644 // this is a bit awkward since backends can store "random stuff" in the
2645 // BackendOpaqueAction nativeId qvariant so we need to tell them to free it
2646 // ideally we would just do that in the BackendOpaqueAction destructor
2647 // but that's too late in the cleanup process, i.e. the generator has already closed its document
2648 // and the document generator is nullptr
2649 for (const Page *p : std::as_const(d->m_pagesVector)) {
2650 const QList<ObjectRect *> &oRects = p->objectRects();
2651 for (const ObjectRect *oRect : oRects) {
2652 if (oRect->objectType() == ObjectRect::Action) {
2653 const Action *a = static_cast<const Action *>(oRect->object());
2654 const BackendOpaqueAction *backendAction = dynamic_cast<const BackendOpaqueAction *>(a);
2655 if (backendAction) {
2656 d->m_generator->freeOpaqueActionContents(*backendAction);
2657 }
2658 }
2659 }
2660
2661 const QList<FormField *> forms = p->formFields();
2662 for (const FormField *form : forms) {
2663 const QList<Action *> additionalActions = form->additionalActions();
2664 for (const Action *a : additionalActions) {
2665 const BackendOpaqueAction *backendAction = dynamic_cast<const BackendOpaqueAction *>(a);
2666 if (backendAction) {
2667 d->m_generator->freeOpaqueActionContents(*backendAction);
2668 }
2669 }
2670 }
2671 }
2672
2673 d->m_generator->closeDocument();
2674 }
2675
2676 if (d->m_synctex_scanner) {
2677 synctex_scanner_free(d->m_synctex_scanner);
2678 d->m_synctex_scanner = nullptr;
2679 }
2680
2681 // stop timers
2682 if (d->m_memCheckTimer) {
2683 d->m_memCheckTimer->stop();
2684 }
2685 if (d->m_saveBookmarksTimer) {
2686 d->m_saveBookmarksTimer->stop();
2687 }
2688
2689 if (d->m_generator) {
2690 // disconnect the generator from this document ...
2691 d->m_generator->d_func()->m_document = nullptr;
2692 // .. and this document from the generator signals
2693 disconnect(d->m_generator, nullptr, this, nullptr);
2694
2695 QHash<QString, GeneratorInfo>::const_iterator genIt = d->m_loadedGenerators.constFind(d->m_generatorName);
2696 Q_ASSERT(genIt != d->m_loadedGenerators.constEnd());
2697 }
2698 d->m_generator = nullptr;
2699 d->m_generatorName = QString();
2700 d->m_url = QUrl();
2701 d->m_walletGenerator = nullptr;
2702 d->m_docFileName = QString();
2703 d->m_xmlFileName = QString();
2704 delete d->m_tempFile;
2705 d->m_tempFile = nullptr;
2706 delete d->m_archiveData;
2707 d->m_archiveData = nullptr;
2708 d->m_docSize = -1;
2709 d->m_exportCached = false;
2710 d->m_exportFormats.clear();
2711 d->m_exportToText = ExportFormat();
2712 d->m_fontsCached = false;
2713 d->m_fontsCache.clear();
2714 d->m_rotation = Rotation0;
2715
2716 // send an empty list to observers (to free their data)
2718
2719 // delete pages and clear 'd->m_pagesVector' container
2720 QVector<Page *>::const_iterator pIt = d->m_pagesVector.constBegin();
2721 QVector<Page *>::const_iterator pEnd = d->m_pagesVector.constEnd();
2722 for (; pIt != pEnd; ++pIt) {
2723 delete *pIt;
2724 }
2725 d->m_pagesVector.clear();
2726
2727 // clear 'memory allocation' descriptors
2728 qDeleteAll(d->m_allocatedPixmaps);
2729 d->m_allocatedPixmaps.clear();
2730
2731 // clear 'running searches' descriptors
2732 QMap<int, RunningSearch *>::const_iterator rIt = d->m_searches.constBegin();
2733 QMap<int, RunningSearch *>::const_iterator rEnd = d->m_searches.constEnd();
2734 for (; rIt != rEnd; ++rIt) {
2735 delete *rIt;
2736 }
2737 d->m_searches.clear();
2738
2739 // clear the visible areas and notify the observers
2740 QVector<VisiblePageRect *>::const_iterator vIt = d->m_pageRects.constBegin();
2741 QVector<VisiblePageRect *>::const_iterator vEnd = d->m_pageRects.constEnd();
2742 for (; vIt != vEnd; ++vIt) {
2743 delete *vIt;
2744 }
2745 d->m_pageRects.clear();
2746 foreachObserver(notifyVisibleRectsChanged());
2747
2748 // reset internal variables
2749
2750 d->m_viewportHistory.clear();
2751 d->m_viewportHistory.emplace_back();
2752 d->m_viewportIterator = d->m_viewportHistory.begin();
2753 d->m_allocatedPixmapsTotalMemory = 0;
2754 d->m_allocatedTextPagesFifo.clear();
2755 d->m_pageSize = PageSize();
2756 d->m_pageSizes.clear();
2757
2758 d->m_documentInfo = DocumentInfo();
2759 d->m_documentInfoAskedKeys.clear();
2760
2761 AudioPlayer::instance()->resetDocument();
2762
2763 d->m_undoStack->clear();
2764 d->m_docdataMigrationNeeded = false;
2765
2766#if HAVE_MALLOC_TRIM
2767 // trim unused memory, glibc should do this but it seems it does not
2768 // this can greatly decrease the [perceived] memory consumption of okular
2769 // see: https://sourceware.org/bugzilla/show_bug.cgi?id=14827
2770 malloc_trim(0);
2771#endif
2772}
2773
2775{
2776 Q_ASSERT(!d->m_observers.contains(pObserver));
2777 d->m_observers << pObserver;
2778
2779 // if the observer is added while a document is already opened, tell it
2780 if (!d->m_pagesVector.isEmpty()) {
2782 pObserver->notifyViewportChanged(false /*disables smoothMove*/);
2783 }
2784}
2785
2787{
2788 // remove observer from the set. it won't receive notifications anymore
2789 if (d->m_observers.contains(pObserver)) {
2790 // free observer's pixmap data
2791 QVector<Page *>::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd();
2792 for (; it != end; ++it) {
2793 (*it)->deletePixmap(pObserver);
2794 }
2795
2796 // [MEM] free observer's allocation descriptors
2797 std::list<AllocatedPixmap *>::iterator aIt = d->m_allocatedPixmaps.begin();
2798 std::list<AllocatedPixmap *>::iterator aEnd = d->m_allocatedPixmaps.end();
2799 while (aIt != aEnd) {
2800 AllocatedPixmap *p = *aIt;
2801 if (p->observer == pObserver) {
2802 aIt = d->m_allocatedPixmaps.erase(aIt);
2803 delete p;
2804 } else {
2805 ++aIt;
2806 }
2807 }
2808
2809 for (PixmapRequest *executingRequest : std::as_const(d->m_executingPixmapRequests)) {
2810 if (executingRequest->observer() == pObserver) {
2811 d->cancelRenderingBecauseOf(executingRequest, nullptr);
2812 }
2813 }
2814
2815 // remove observer entry from the set
2816 d->m_observers.remove(pObserver);
2817 }
2818}
2819
2821{
2822 // reparse generator config and if something changed clear Pages
2823 bool configchanged = false;
2824 if (d->m_generator) {
2826 if (iface) {
2827 configchanged = iface->reparseConfig();
2828 }
2829 }
2830 if (configchanged) {
2831 // invalidate pixmaps
2832 QVector<Page *>::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd();
2833 for (; it != end; ++it) {
2834 (*it)->deletePixmaps();
2835 }
2836
2837 // [MEM] remove allocation descriptors
2838 qDeleteAll(d->m_allocatedPixmaps);
2839 d->m_allocatedPixmaps.clear();
2840 d->m_allocatedPixmapsTotalMemory = 0;
2841
2842 // send reload signals to observers
2843 foreachObserver(notifyContentsCleared(DocumentObserver::Pixmap));
2844 }
2845
2846 // free memory if in 'low' profile
2847 if (SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !d->m_allocatedPixmaps.empty() && !d->m_pagesVector.isEmpty()) {
2848 d->cleanupPixmapMemory();
2849 }
2850}
2851
2853{
2854 return d->m_generator;
2855}
2856
2858{
2859 if (d->m_generator) {
2861 return iface ? true : false;
2862 } else {
2863 return false;
2864 }
2865}
2866
2867bool Document::sign(const NewSignatureData &data, const QString &newPath)
2868{
2869 if (d->m_generator->canSign()) {
2870 return d->m_generator->sign(data, newPath);
2871 } else {
2872 return false;
2873 }
2874}
2875
2877{
2878 return d->m_generator ? d->m_generator->certificateStore() : nullptr;
2879}
2880
2882{
2883 d->editorCommandOverride = editCmd;
2884}
2885
2887{
2888 return d->editorCommandOverride;
2889}
2890
2892{
2895 keys << ks;
2896 }
2897
2898 return documentInfo(keys);
2899}
2900
2902{
2903 DocumentInfo result = d->m_documentInfo;
2904 const QSet<DocumentInfo::Key> missingKeys = keys - d->m_documentInfoAskedKeys;
2905
2906 if (d->m_generator && !missingKeys.isEmpty()) {
2907 DocumentInfo info = d->m_generator->generateDocumentInfo(missingKeys);
2908
2909 if (missingKeys.contains(DocumentInfo::FilePath)) {
2910 info.set(DocumentInfo::FilePath, currentDocument().toDisplayString());
2911 }
2912
2913 if (d->m_docSize != -1 && missingKeys.contains(DocumentInfo::DocumentSize)) {
2914 const QString sizeString = KFormat().formatByteSize(d->m_docSize);
2915 info.set(DocumentInfo::DocumentSize, sizeString);
2916 }
2917 if (missingKeys.contains(DocumentInfo::PagesSize)) {
2918 const QString pagesSize = d->pagesSizeString();
2919 if (!pagesSize.isEmpty()) {
2920 info.set(DocumentInfo::PagesSize, pagesSize);
2921 }
2922 }
2923
2924 if (missingKeys.contains(DocumentInfo::Pages) && info.get(DocumentInfo::Pages).isEmpty()) {
2926 }
2927
2928 d->m_documentInfo.d->values.insert(info.d->values);
2929 d->m_documentInfo.d->titles.insert(info.d->titles);
2930 result.d->values.insert(info.d->values);
2931 result.d->titles.insert(info.d->titles);
2932 }
2933 d->m_documentInfoAskedKeys += keys;
2934
2935 return result;
2936}
2937
2939{
2940 return d->m_generator ? d->m_generator->generateDocumentSynopsis() : nullptr;
2941}
2942
2944{
2945 if (!d->m_generator || !d->m_generator->hasFeature(Generator::FontInfo) || d->m_fontThread) {
2946 return;
2947 }
2948
2949 if (d->m_fontsCached) {
2950 // in case we have cached fonts, simulate a reading
2951 // this way the API is the same, and users no need to care about the
2952 // internal caching
2953 for (int i = 0; i < d->m_fontsCache.count(); ++i) {
2954 Q_EMIT gotFont(d->m_fontsCache.at(i));
2956 }
2958 return;
2959 }
2960
2961 d->m_fontThread = new FontExtractionThread(d->m_generator, pages());
2962 connect(d->m_fontThread, &FontExtractionThread::gotFont, this, [this](const Okular::FontInfo &f) { d->fontReadingGotFont(f); });
2963 connect(d->m_fontThread.data(), &FontExtractionThread::progress, this, [this](int p) { d->slotFontReadingProgress(p); });
2964
2965 d->m_fontThread->startExtraction(/*d->m_generator->hasFeature( Generator::Threaded )*/ true);
2966}
2967
2969{
2970 if (!d->m_fontThread) {
2971 return;
2972 }
2973
2974 disconnect(d->m_fontThread, nullptr, this, nullptr);
2975 d->m_fontThread->stopExtraction();
2976 d->m_fontThread = nullptr;
2977 d->m_fontsCache.clear();
2978}
2979
2981{
2982 return d->m_generator ? d->m_generator->hasFeature(Generator::FontInfo) : false;
2983}
2984
2986{
2987 return d->m_generator ? d->m_generator->canSign() : false;
2988}
2989
2991{
2992 return d->m_generator ? d->m_generator->embeddedFiles() : nullptr;
2993}
2994
2995const Page *Document::page(int n) const
2996{
2997 return (n >= 0 && n < d->m_pagesVector.count()) ? d->m_pagesVector.at(n) : nullptr;
2998}
2999
3001{
3002 return (*d->m_viewportIterator);
3003}
3004
3006{
3007 return d->m_pageRects;
3008}
3009
3011{
3012 QVector<VisiblePageRect *>::const_iterator vIt = d->m_pageRects.constBegin();
3013 QVector<VisiblePageRect *>::const_iterator vEnd = d->m_pageRects.constEnd();
3014 for (; vIt != vEnd; ++vIt) {
3015 delete *vIt;
3016 }
3017 d->m_pageRects = visiblePageRects;
3018 // notify change to all other (different from id) observers
3019 for (DocumentObserver *o : std::as_const(d->m_observers)) {
3020 if (o != excludeObserver) {
3021 o->notifyVisibleRectsChanged();
3022 }
3023 }
3024}
3025
3027{
3028 return (*d->m_viewportIterator).pageNumber;
3029}
3030
3032{
3033 return d->m_pagesVector.size();
3034}
3035
3037{
3038 return d->m_url;
3039}
3040
3042{
3043 if (action == Okular::AllowNotes && (d->m_docdataMigrationNeeded || !d->m_annotationEditingEnabled)) {
3044 return false;
3045 }
3046 if (action == Okular::AllowFillForms && d->m_docdataMigrationNeeded) {
3047 return false;
3048 }
3049
3050#if !OKULAR_FORCE_DRM
3051 if (KAuthorized::authorize(QStringLiteral("skip_drm")) && !SettingsCore::obeyDRM()) {
3052 return true;
3053 }
3054#endif
3055
3056 return d->m_generator ? d->m_generator->isAllowed(action) : false;
3057}
3058
3060{
3061 return d->m_generator ? d->m_generator->hasFeature(Generator::TextExtraction) : false;
3062}
3063
3065{
3066 return d->m_generator ? d->m_generator->hasFeature(Generator::PageSizes) : false;
3067}
3068
3070{
3071 return d->m_generator ? d->m_generator->hasFeature(Generator::TiledRendering) : false;
3072}
3073
3075{
3076 if (d->m_generator) {
3077 if (d->m_pageSizes.isEmpty()) {
3078 d->m_pageSizes = d->m_generator->pageSizes();
3079 }
3080 return d->m_pageSizes;
3081 }
3082 return PageSize::List();
3083}
3084
3086{
3087 if (!d->m_generator) {
3088 return false;
3089 }
3090
3091 d->cacheExportFormats();
3092 return !d->m_exportToText.isNull();
3093}
3094
3095bool Document::exportToText(const QString &fileName) const
3096{
3097 if (!d->m_generator) {
3098 return false;
3099 }
3100
3101 d->cacheExportFormats();
3102 if (d->m_exportToText.isNull()) {
3103 return false;
3104 }
3105
3106 return d->m_generator->exportTo(fileName, d->m_exportToText);
3107}
3108
3110{
3111 if (!d->m_generator) {
3112 return ExportFormat::List();
3113 }
3114
3115 d->cacheExportFormats();
3116 return d->m_exportFormats;
3117}
3118
3119bool Document::exportTo(const QString &fileName, const ExportFormat &format) const
3120{
3121 return d->m_generator ? d->m_generator->exportTo(fileName, format) : false;
3122}
3123
3125{
3126 return d->m_viewportIterator == d->m_viewportHistory.begin();
3127}
3128
3130{
3131 return d->m_viewportIterator == --(d->m_viewportHistory.end());
3132}
3133
3134QVariant Document::metaData(const QString &key, const QVariant &option) const
3135{
3136 // if option starts with "src:" assume that we are handling a
3137 // source reference
3138 if (key == QLatin1String("NamedViewport") && option.toString().startsWith(QLatin1String("src:"), Qt::CaseInsensitive) && d->m_synctex_scanner) {
3139 const QString reference = option.toString();
3140
3141 // The reference is of form "src:1111Filename", where "1111"
3142 // points to line number 1111 in the file "Filename".
3143 // Extract the file name and the numeral part from the reference string.
3144 // This will fail if Filename starts with a digit.
3145 QString name, lineString;
3146 // Remove "src:". Presence of substring has been checked before this
3147 // function is called.
3148 name = reference.mid(4);
3149 // split
3150 int nameLength = name.length();
3151 int i = 0;
3152 for (i = 0; i < nameLength; ++i) {
3153 if (!name[i].isDigit()) {
3154 break;
3155 }
3156 }
3157 lineString = name.left(i);
3158 name = name.mid(i);
3159 // Remove spaces.
3160 name = name.trimmed();
3161 lineString = lineString.trimmed();
3162 // Convert line to integer.
3163 bool ok;
3164 int line = lineString.toInt(&ok);
3165 if (!ok) {
3166 line = -1;
3167 }
3168
3169 // Use column == -1 for now.
3170 if (synctex_display_query(d->m_synctex_scanner, QFile::encodeName(name).constData(), line, -1, 0) > 0) {
3171 synctex_node_p node;
3172 // For now use the first hit. Could possibly be made smarter
3173 // in case there are multiple hits.
3174 while ((node = synctex_scanner_next_result(d->m_synctex_scanner))) {
3176
3177 // TeX pages start at 1.
3178 viewport.pageNumber = synctex_node_page(node) - 1;
3179
3180 if (viewport.pageNumber >= 0) {
3181 const QSizeF dpi = d->m_generator->dpi();
3182
3183 // TeX small points ...
3184 double px = (synctex_node_visible_h(node) * dpi.width()) / 72.27;
3185 double py = (synctex_node_visible_v(node) * dpi.height()) / 72.27;
3186 viewport.rePos.normalizedX = px / page(viewport.pageNumber)->width();
3187 viewport.rePos.normalizedY = (py + 0.5) / page(viewport.pageNumber)->height();
3188 viewport.rePos.enabled = true;
3190
3191 return viewport.toString();
3192 }
3193 }
3194 }
3195 }
3196 return d->m_generator ? d->m_generator->metaData(key, option) : QVariant();
3197}
3198
3200{
3201 return d->m_rotation;
3202}
3203
3205{
3206 bool allPagesSameSize = true;
3207 QSizeF size;
3208 for (int i = 0; allPagesSameSize && i < d->m_pagesVector.count(); ++i) {
3209 const Page *p = d->m_pagesVector.at(i);
3210 if (i == 0) {
3211 size = QSizeF(p->width(), p->height());
3212 } else {
3213 allPagesSameSize = (size == QSizeF(p->width(), p->height()));
3214 }
3215 }
3216 if (allPagesSameSize) {
3217 return size;
3218 } else {
3219 return QSizeF();
3220 }
3221}
3222
3224{
3225 if (d->m_generator) {
3226 if (d->m_generator->pagesSizeMetric() != Generator::None) {
3227 const Page *p = d->m_pagesVector.at(page);
3228 return d->localizedSize(QSizeF(p->width(), p->height()));
3229 }
3230 }
3231 return QString();
3232}
3233
3234static bool shouldCancelRenderingBecauseOf(const PixmapRequest &executingRequest, const PixmapRequest &otherRequest)
3235{
3236 // New request has higher priority -> cancel
3237 if (executingRequest.priority() > otherRequest.priority()) {
3238 return true;
3239 }
3240
3241 // New request has lower priority -> don't cancel
3242 if (executingRequest.priority() < otherRequest.priority()) {
3243 return false;
3244 }
3245
3246 // New request has same priority and is from a different observer -> don't cancel
3247 // AFAIK this never happens since all observers have different priorities
3248 if (executingRequest.observer() != otherRequest.observer()) {
3249 return false;
3250 }
3251
3252 // Same priority and observer, different page number -> don't cancel
3253 // may still end up cancelled later in the parent caller if none of the requests
3254 // is of the executingRequest page and RemoveAllPrevious is specified
3255 if (executingRequest.pageNumber() != otherRequest.pageNumber()) {
3256 return false;
3257 }
3258
3259 // Same priority, observer, page, different size -> cancel
3260 if (executingRequest.width() != otherRequest.width()) {
3261 return true;
3262 }
3263
3264 // Same priority, observer, page, different size -> cancel
3265 if (executingRequest.height() != otherRequest.height()) {
3266 return true;
3267 }
3268
3269 // Same priority, observer, page, different tiling -> cancel
3270 if (executingRequest.isTile() != otherRequest.isTile()) {
3271 return true;
3272 }
3273
3274 // Same priority, observer, page, different tiling -> cancel
3275 if (executingRequest.isTile()) {
3276 const NormalizedRect bothRequestsRect = executingRequest.normalizedRect() | otherRequest.normalizedRect();
3277 if (!(bothRequestsRect == executingRequest.normalizedRect())) {
3278 return true;
3279 }
3280 }
3281
3282 return false;
3283}
3284
3285bool DocumentPrivate::cancelRenderingBecauseOf(PixmapRequest *executingRequest, PixmapRequest *newRequest)
3286{
3287 // No point in aborting the rendering already finished, let it go through
3288 if (!executingRequest->d->mResultImage.isNull()) {
3289 return false;
3290 }
3291
3292 if (newRequest && newRequest->asynchronous() && executingRequest->partialUpdatesWanted()) {
3293 newRequest->setPartialUpdatesWanted(true);
3294 }
3295
3296 TilesManager *tm = executingRequest->d->tilesManager();
3297 if (tm) {
3298 tm->setPixmap(nullptr, executingRequest->normalizedRect(), true /*isPartialPixmap*/);
3299 tm->setRequest(NormalizedRect(), 0, 0);
3300 }
3301 PagePrivate::PixmapObject object = executingRequest->page()->d->m_pixmaps.take(executingRequest->observer());
3302 delete object.m_pixmap;
3303
3304 if (executingRequest->d->mShouldAbortRender != 0) {
3305 return false;
3306 }
3307
3308 executingRequest->d->mShouldAbortRender = 1;
3309
3310 if (m_generator->d_ptr->mTextPageGenerationThread && m_generator->d_ptr->mTextPageGenerationThread->page() == executingRequest->page()) {
3311 m_generator->d_ptr->mTextPageGenerationThread->abortExtraction();
3312 }
3313
3314 return true;
3315}
3316
3318{
3320}
3321
3323{
3324 if (requests.isEmpty()) {
3325 return;
3326 }
3327
3328 if (!d->m_pageController) {
3329 // delete requests..
3330 qDeleteAll(requests);
3331 // ..and return
3332 return;
3333 }
3334
3335 QSet<DocumentObserver *> observersPixmapCleared;
3336
3337 // 1. [CLEAN STACK] remove previous requests of requesterID
3338 const DocumentObserver *requesterObserver = requests.first()->observer();
3339 QSet<int> requestedPages;
3340 {
3341 for (const PixmapRequest *request : requests) {
3342 Q_ASSERT(request->observer() == requesterObserver);
3343 requestedPages.insert(request->pageNumber());
3344 }
3345 }
3346 const bool removeAllPrevious = reqOptions & RemoveAllPrevious;
3347 d->m_pixmapRequestsMutex.lock();
3348 std::list<PixmapRequest *>::iterator sIt = d->m_pixmapRequestsStack.begin(), sEnd = d->m_pixmapRequestsStack.end();
3349 while (sIt != sEnd) {
3350 if ((*sIt)->observer() == requesterObserver && (removeAllPrevious || requestedPages.contains((*sIt)->pageNumber()))) {
3351 // delete request and remove it from stack
3352 delete *sIt;
3353 sIt = d->m_pixmapRequestsStack.erase(sIt);
3354 } else {
3355 ++sIt;
3356 }
3357 }
3358
3359 // 1.B [PREPROCESS REQUESTS] tweak some values of the requests
3360 for (PixmapRequest *request : requests) {
3361 // set the 'page field' (see PixmapRequest) and check if it is valid
3362 qCDebug(OkularCoreDebug).nospace() << "request observer=" << request->observer() << " " << request->width() << "x" << request->height() << "@" << request->pageNumber();
3363 if (d->m_pagesVector.value(request->pageNumber()) == nullptr) {
3364 // skip requests referencing an invalid page (must not happen)
3365 delete request;
3366 continue;
3367 }
3368
3369 request->d->mPage = d->m_pagesVector.value(request->pageNumber());
3370
3371 if (request->isTile()) {
3372 // Change the current request rect so that only invalid tiles are
3373 // requested. Also make sure the rect is tile-aligned.
3374 NormalizedRect tilesRect;
3375 const QList<Tile> tiles = request->d->tilesManager()->tilesAt(request->normalizedRect(), TilesManager::TerminalTile);
3376 QList<Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
3377 while (tIt != tEnd) {
3378 const Tile &tile = *tIt;
3379 if (!tile.isValid()) {
3380 if (tilesRect.isNull()) {
3381 tilesRect = tile.rect();
3382 } else {
3383 tilesRect |= tile.rect();
3384 }
3385 }
3386
3387 tIt++;
3388 }
3389
3390 request->setNormalizedRect(tilesRect);
3391 }
3392
3393 if (!request->asynchronous()) {
3394 request->d->mPriority = 0;
3395 }
3396 }
3397
3398 // 1.C [CANCEL REQUESTS] cancel those requests that are running and should be cancelled because of the new requests coming in
3399 if (d->m_generator->hasFeature(Generator::SupportsCancelling)) {
3400 for (PixmapRequest *executingRequest : std::as_const(d->m_executingPixmapRequests)) {
3401 bool newRequestsContainExecutingRequestPage = false;
3402 bool requestCancelled = false;
3403 for (PixmapRequest *newRequest : requests) {
3404 if (newRequest->pageNumber() == executingRequest->pageNumber() && requesterObserver == executingRequest->observer()) {
3405 newRequestsContainExecutingRequestPage = true;
3406 }
3407
3408 if (shouldCancelRenderingBecauseOf(*executingRequest, *newRequest)) {
3409 requestCancelled = d->cancelRenderingBecauseOf(executingRequest, newRequest);
3410 }
3411 }
3412
3413 // If we were told to remove all the previous requests and the executing request page is not part of the new requests, cancel it
3414 if (!requestCancelled && removeAllPrevious && requesterObserver == executingRequest->observer() && !newRequestsContainExecutingRequestPage) {
3415 requestCancelled = d->cancelRenderingBecauseOf(executingRequest, nullptr);
3416 }
3417
3418 if (requestCancelled) {
3419 observersPixmapCleared << executingRequest->observer();
3420 }
3421 }
3422 }
3423
3424 // 2. [ADD TO STACK] add requests to stack
3425 for (PixmapRequest *request : requests) {
3426 // add request to the 'stack' at the right place
3427 if (!request->priority()) {
3428 // add priority zero requests to the top of the stack
3429 d->m_pixmapRequestsStack.push_back(request);
3430 } else {
3431 // insert in stack sorted by priority
3432 sIt = d->m_pixmapRequestsStack.begin();
3433 sEnd = d->m_pixmapRequestsStack.end();
3434 while (sIt != sEnd && (*sIt)->priority() > request->priority()) {
3435 ++sIt;
3436 }
3437 d->m_pixmapRequestsStack.insert(sIt, request);
3438 }
3439 }
3440 d->m_pixmapRequestsMutex.unlock();
3441
3442 // 3. [START FIRST GENERATION] if <NO>generator is ready, start a new generation,
3443 // or else (if gen is running) it will be started when the new contents will
3444 // come from generator (in requestDone())</NO>
3445 // all handling of requests put into sendGeneratorPixmapRequest
3446 // if ( generator->canRequestPixmap() )
3447 d->sendGeneratorPixmapRequest();
3448
3449 for (DocumentObserver *o : std::as_const(observersPixmapCleared)) {
3450 o->notifyContentsCleared(Okular::DocumentObserver::Pixmap);
3451 }
3452}
3453
3454void Document::requestTextPage(uint pageNumber)
3455{
3456 Page *kp = d->m_pagesVector[pageNumber];
3457 if (!d->m_generator || !kp) {
3458 return;
3459 }
3460
3461 // Memory management for TextPages
3462
3463 d->m_generator->generateTextPage(kp);
3464}
3465
3466void DocumentPrivate::notifyAnnotationChanges(int page)
3467{
3468 foreachObserverD(notifyPageChanged(page, DocumentObserver::Annotations));
3469}
3470
3471void DocumentPrivate::notifyFormChanges(int /*page*/)
3472{
3473 recalculateForms();
3474}
3475
3476void Document::addPageAnnotation(int page, Annotation *annotation)
3477{
3478 // Transform annotation's base boundary rectangle into unrotated coordinates
3479 Page *p = d->m_pagesVector[page];
3480 QTransform t = p->d->rotationMatrix();
3481 annotation->d_ptr->baseTransform(t.inverted());
3482 QUndoCommand *uc = new AddAnnotationCommand(this->d, annotation, page);
3483 d->m_undoStack->push(uc);
3484}
3485
3487{
3488 if (!annotation || (annotation->flags() & Annotation::DenyWrite)) {
3489 return false;
3490 }
3491
3493 return false;
3494 }
3495
3496 if ((annotation->flags() & Annotation::External) && !d->canModifyExternalAnnotations()) {
3497 return false;
3498 }
3499
3500 switch (annotation->subType()) {
3501 case Annotation::AText:
3502 case Annotation::ALine:
3503 case Annotation::AGeom:
3505 case Annotation::AStamp:
3506 case Annotation::AInk:
3507 return true;
3508 default:
3509 return false;
3510 }
3511}
3512
3514{
3515 Q_ASSERT(d->m_prevPropsOfAnnotBeingModified.isNull());
3516 if (!d->m_prevPropsOfAnnotBeingModified.isNull()) {
3517 qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties has already been called since last call to Document::modifyPageAnnotationProperties";
3518 return;
3519 }
3520 d->m_prevPropsOfAnnotBeingModified = annotation->getAnnotationPropertiesDomNode();
3521}
3522
3524{
3525 Q_ASSERT(!d->m_prevPropsOfAnnotBeingModified.isNull());
3526 if (d->m_prevPropsOfAnnotBeingModified.isNull()) {
3527 qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties must be called before Annotation is modified";
3528 return;
3529 }
3530 QDomNode prevProps = d->m_prevPropsOfAnnotBeingModified;
3531 QUndoCommand *uc = new Okular::ModifyAnnotationPropertiesCommand(d, annotation, page, prevProps, annotation->getAnnotationPropertiesDomNode());
3532 d->m_undoStack->push(uc);
3533 d->m_prevPropsOfAnnotBeingModified.clear();
3534}
3535
3536void Document::translatePageAnnotation(int page, Annotation *annotation, const NormalizedPoint &delta)
3537{
3538 int complete = (annotation->flags() & Okular::Annotation::BeingMoved) == 0;
3539 QUndoCommand *uc = new Okular::TranslateAnnotationCommand(d, annotation, page, delta, complete);
3540 d->m_undoStack->push(uc);
3541}
3542
3543void Document::adjustPageAnnotation(int page, Annotation *annotation, const Okular::NormalizedPoint &delta1, const Okular::NormalizedPoint &delta2)
3544{
3545 const bool complete = (annotation->flags() & Okular::Annotation::BeingResized) == 0;
3546 QUndoCommand *uc = new Okular::AdjustAnnotationCommand(d, annotation, page, delta1, delta2, complete);
3547 d->m_undoStack->push(uc);
3548}
3549
3550void Document::editPageAnnotationContents(int page, Annotation *annotation, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3551{
3552 QString prevContents = annotation->contents();
3553 QUndoCommand *uc = new EditAnnotationContentsCommand(d, annotation, page, newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos);
3554 d->m_undoStack->push(uc);
3555}
3556
3558{
3559 if (!annotation || (annotation->flags() & Annotation::DenyDelete)) {
3560 return false;
3561 }
3562
3563 if ((annotation->flags() & Annotation::External) && !d->canRemoveExternalAnnotations()) {
3564 return false;
3565 }
3566
3567 switch (annotation->subType()) {
3568 case Annotation::AText:
3569 case Annotation::ALine:
3570 case Annotation::AGeom:
3572 case Annotation::AStamp:
3573 case Annotation::AInk:
3574 case Annotation::ACaret:
3575 return true;
3576 default:
3577 return false;
3578 }
3579}
3580
3582{
3583 QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page);
3584 d->m_undoStack->push(uc);
3585}
3586
3588{
3589 d->m_undoStack->beginMacro(i18nc("remove a collection of annotations from the page", "remove annotations"));
3590 for (Annotation *annotation : annotations) {
3591 QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page);
3592 d->m_undoStack->push(uc);
3593 }
3594 d->m_undoStack->endMacro();
3595}
3596
3597bool DocumentPrivate::canAddAnnotationsNatively() const
3598{
3599 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3600
3602 return true;
3603 }
3604
3605 return false;
3606}
3607
3608bool DocumentPrivate::canModifyExternalAnnotations() const
3609{
3610 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3611
3613 return true;
3614 }
3615
3616 return false;
3617}
3618
3619bool DocumentPrivate::canRemoveExternalAnnotations() const
3620{
3621 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3622
3624 return true;
3625 }
3626
3627 return false;
3628}
3629
3630void Document::setPageTextSelection(int page, std::unique_ptr<RegularAreaRect> &&rect, const QColor &color)
3631{
3632 Page *kp = d->m_pagesVector[page];
3633 if (!d->m_generator || !kp) {
3634 return;
3635 }
3636
3637 // add or remove the selection basing whether rect is null or not
3638 if (rect) {
3639 kp->d->setTextSelections(*rect, color);
3640 } else {
3641 kp->d->deleteTextSelections();
3642 }
3643
3644 // notify observers about the change
3645 foreachObserver(notifyPageChanged(page, DocumentObserver::TextSelection));
3646}
3647
3649{
3650 return d->m_undoStack->canUndo();
3651}
3652
3654{
3655 return d->m_undoStack->canRedo();
3656}
3657
3658/* REFERENCE IMPLEMENTATION: better calling setViewport from other code
3659void Document::setNextPage()
3660{
3661 // advance page and set viewport on observers
3662 if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 )
3663 setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber + 1 ) );
3664}
3665
3666void Document::setPrevPage()
3667{
3668 // go to previous page and set viewport on observers
3669 if ( (*d->m_viewportIterator).pageNumber > 0 )
3670 setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber - 1 ) );
3671}
3672*/
3673
3674void Document::setViewport(const DocumentViewport &viewport, DocumentObserver *excludeObserver, bool smoothMove, bool updateHistory)
3675{
3676 if (!viewport.isValid()) {
3677 qCDebug(OkularCoreDebug) << "invalid viewport:" << viewport.toString();
3678 return;
3679 }
3680 if (viewport.pageNumber >= int(d->m_pagesVector.count())) {
3681 // qCDebug(OkularCoreDebug) << "viewport out of document:" << viewport.toString();
3682 return;
3683 }
3684
3685 // if already broadcasted, don't redo it
3686 DocumentViewport &oldViewport = *d->m_viewportIterator;
3687 // disabled by enrico on 2005-03-18 (less debug output)
3688 // if ( viewport == oldViewport )
3689 // qCDebug(OkularCoreDebug) << "setViewport with the same viewport.";
3690
3691 const int oldPageNumber = oldViewport.pageNumber;
3692
3693 // set internal viewport taking care of history
3694 if (oldViewport.pageNumber == viewport.pageNumber || !oldViewport.isValid() || !updateHistory) {
3695 // if page is unchanged save the viewport at current position in queue
3696 oldViewport = viewport;
3697 } else {
3698 // remove elements after viewportIterator in queue
3699 d->m_viewportHistory.erase(++d->m_viewportIterator, d->m_viewportHistory.end());
3700
3701 // keep the list to a reasonable size by removing head when needed
3702 if (d->m_viewportHistory.size() >= OKULAR_HISTORY_MAXSTEPS) {
3703 d->m_viewportHistory.pop_front();
3704 }
3705
3706 // add the item at the end of the queue
3707 d->m_viewportIterator = d->m_viewportHistory.insert(d->m_viewportHistory.end(), viewport);
3708 }
3709
3710 const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3711
3712 const bool currentPageChanged = (oldPageNumber != currentViewportPage);
3713
3714 // notify change to all other (different from id) observers
3715 for (DocumentObserver *o : std::as_const(d->m_observers)) {
3716 if (o != excludeObserver) {
3717 o->notifyViewportChanged(smoothMove);
3718 }
3719
3720 if (currentPageChanged) {
3721 o->notifyCurrentPageChanged(oldPageNumber, currentViewportPage);
3722 }
3723 }
3724}
3725
3726void Document::setViewportPage(int page, DocumentObserver *excludeObserver, bool smoothMove)
3727{
3728 // clamp page in range [0 ... numPages-1]
3729 if (page < 0) {
3730 page = 0;
3731 } else if (page > (int)d->m_pagesVector.count()) {
3732 page = d->m_pagesVector.count() - 1;
3733 }
3734
3735 // make a viewport from the page and broadcast it
3736 setViewport(DocumentViewport(page), excludeObserver, smoothMove);
3737}
3738
3739void Document::setZoom(int factor, DocumentObserver *excludeObserver)
3740{
3741 // notify change to all other (different from id) observers
3742 for (DocumentObserver *o : std::as_const(d->m_observers)) {
3743 if (o != excludeObserver) {
3744 o->notifyZoom(factor);
3745 }
3746 }
3747}
3748
3750// restore viewport from the history
3751{
3752 if (d->m_viewportIterator != d->m_viewportHistory.begin()) {
3753 const int oldViewportPage = (*d->m_viewportIterator).pageNumber;
3754
3755 // restore previous viewport and notify it to observers
3756 --d->m_viewportIterator;
3757 foreachObserver(notifyViewportChanged(true));
3758
3759 const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3760 if (oldViewportPage != currentViewportPage)
3761 foreachObserver(notifyCurrentPageChanged(oldViewportPage, currentViewportPage));
3762 }
3763}
3764
3766// restore next viewport from the history
3767{
3768 auto nextIterator = std::list<DocumentViewport>::const_iterator(d->m_viewportIterator);
3769 ++nextIterator;
3770 if (nextIterator != d->m_viewportHistory.end()) {
3771 const int oldViewportPage = (*d->m_viewportIterator).pageNumber;
3772
3773 // restore next viewport and notify it to observers
3774 ++d->m_viewportIterator;
3775 foreachObserver(notifyViewportChanged(true));
3776
3777 const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3778 if (oldViewportPage != currentViewportPage)
3779 foreachObserver(notifyCurrentPageChanged(oldViewportPage, currentViewportPage));
3780 }
3781}
3782
3784{
3785 d->m_nextDocumentViewport = viewport;
3786}
3787
3789{
3790 d->m_nextDocumentDestination = namedDestination;
3791}
3792
3793void Document::searchText(int searchID, const QString &text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor &color)
3794{
3795 d->m_searchCancelled = false;
3796
3797 // safety checks: don't perform searches on empty or unsearchable docs
3798 if (!d->m_generator || !d->m_generator->hasFeature(Generator::TextExtraction) || d->m_pagesVector.isEmpty()) {
3800 return;
3801 }
3802
3803 // if searchID search not recorded, create new descriptor and init params
3804 QMap<int, RunningSearch *>::iterator searchIt = d->m_searches.find(searchID);
3805 if (searchIt == d->m_searches.end()) {
3806 RunningSearch *search = new RunningSearch();
3807 search->continueOnPage = -1;
3808 searchIt = d->m_searches.insert(searchID, search);
3809 }
3810 RunningSearch *s = *searchIt;
3811
3812 // update search structure
3813 bool newText = text != s->cachedString;
3814 s->cachedString = text;
3815 s->cachedType = type;
3816 s->cachedCaseSensitivity = caseSensitivity;
3817 s->cachedViewportMove = moveViewport;
3818 s->cachedColor = color;
3819 s->isCurrentlySearching = true;
3820
3821 // global data for search
3822 QSet<int> *pagesToNotify = new QSet<int>;
3823
3824 // remove highlights from pages and queue them for notifying changes
3825 *pagesToNotify += s->highlightedPages;
3826 for (const int pageNumber : std::as_const(s->highlightedPages)) {
3827 d->m_pagesVector.at(pageNumber)->d->deleteHighlights(searchID);
3828 }
3829 s->highlightedPages.clear();
3830
3831 // set hourglass cursor
3833
3834 // 1. ALLDOC - process all document marking pages
3835 if (type == AllDocument) {
3837
3838 // search and highlight 'text' (as a solid phrase) on all pages
3839 QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID] { d->doContinueAllDocumentSearch(pagesToNotify, pageMatches, 0, searchID); });
3840 }
3841 // 2. NEXTMATCH - find next matching item (or start from top)
3842 // 3. PREVMATCH - find previous matching item (or start from bottom)
3843 else if (type == NextMatch || type == PreviousMatch) {
3844 // find out from where to start/resume search from
3845 const bool forward = type == NextMatch;
3846 const int viewportPage = (*d->m_viewportIterator).pageNumber;
3847 const int fromStartSearchPage = forward ? 0 : d->m_pagesVector.count() - 1;
3848 int currentPage = fromStart ? fromStartSearchPage : ((s->continueOnPage != -1) ? s->continueOnPage : viewportPage);
3849 const Page *lastPage = fromStart ? nullptr : d->m_pagesVector[currentPage];
3850 int pagesDone = 0;
3851
3852 // continue checking last TextPage first (if it is the current page)
3853 RegularAreaRect *match = nullptr;
3854 if (lastPage && lastPage->number() == s->continueOnPage) {
3855 if (newText) {
3856 match = lastPage->findText(searchID, text, forward ? FromTop : FromBottom, caseSensitivity);
3857 } else {
3858 match = lastPage->findText(searchID, text, forward ? NextResult : PreviousResult, caseSensitivity, &s->continueOnMatch);
3859 }
3860 if (!match) {
3861 if (forward) {
3862 currentPage++;
3863 } else {
3864 currentPage--;
3865 }
3866 pagesDone++;
3867 }
3868 }
3869
3870 s->pagesDone = pagesDone;
3871
3872 DoContinueDirectionMatchSearchStruct *searchStruct = new DoContinueDirectionMatchSearchStruct();
3873 searchStruct->pagesToNotify = pagesToNotify;
3874 searchStruct->match = match;
3875 searchStruct->currentPage = currentPage;
3876 searchStruct->searchID = searchID;
3877
3878 QTimer::singleShot(0, this, [this, searchStruct] { d->doContinueDirectionMatchSearch(searchStruct); });
3879 }
3880 // 4. GOOGLE* - process all document marking pages
3881 else if (type == GoogleAll || type == GoogleAny) {
3883 const QStringList words = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
3884
3885 // search and highlight every word in 'text' on all pages
3886 QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID, words] { d->doContinueGooglesDocumentSearch(pagesToNotify, pageMatches, 0, searchID, words); });
3887 }
3888}
3889
3891{
3892 // check if searchID is present in runningSearches
3893 QMap<int, RunningSearch *>::const_iterator it = d->m_searches.constFind(searchID);
3894 if (it == d->m_searches.constEnd()) {
3896 return;
3897 }
3898
3899 // start search with cached parameters from last search by searchID
3900 RunningSearch *p = *it;
3901 if (!p->isCurrentlySearching) {
3902 searchText(searchID, p->cachedString, false, p->cachedCaseSensitivity, p->cachedType, p->cachedViewportMove, p->cachedColor);
3903 }
3904}
3905
3906void Document::continueSearch(int searchID, SearchType type)
3907{
3908 // check if searchID is present in runningSearches
3909 QMap<int, RunningSearch *>::const_iterator it = d->m_searches.constFind(searchID);
3910 if (it == d->m_searches.constEnd()) {
3912 return;
3913 }
3914
3915 // start search with cached parameters from last search by searchID
3916 RunningSearch *p = *it;
3917 if (!p->isCurrentlySearching) {
3918 searchText(searchID, p->cachedString, false, p->cachedCaseSensitivity, type, p->cachedViewportMove, p->cachedColor);
3919 }
3920}
3921
3922void Document::resetSearch(int searchID)
3923{
3924 // if we are closing down, don't bother doing anything
3925 if (!d->m_generator) {
3926 return;
3927 }
3928
3929 // check if searchID is present in runningSearches
3930 QMap<int, RunningSearch *>::iterator searchIt = d->m_searches.find(searchID);
3931 if (searchIt == d->m_searches.end()) {
3932 return;
3933 }
3934
3935 // get previous parameters for search
3936 RunningSearch *s = *searchIt;
3937
3938 // unhighlight pages and inform observers about that
3939 for (const int pageNumber : std::as_const(s->highlightedPages)) {
3940 d->m_pagesVector.at(pageNumber)->d->deleteHighlights(searchID);
3941 foreachObserver(notifyPageChanged(pageNumber, DocumentObserver::Highlights));
3942 }
3943
3944 // send the setup signal too (to update views that filter on matches)
3945 foreachObserver(notifySetup(d->m_pagesVector, 0));
3946
3947 // remove search from the runningSearches list and delete it
3948 d->m_searches.erase(searchIt);
3949 delete s;
3950}
3951
3953{
3954 d->m_searchCancelled = true;
3955}
3956
3958{
3959 d->m_undoStack->undo();
3960}
3961
3963{
3964 d->m_undoStack->redo();
3965}
3966
3967void Document::editFormText(int pageNumber, Okular::FormFieldText *form, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3968{
3969 QUndoCommand *uc = new EditFormTextCommand(this->d, form, pageNumber, newContents, newCursorPos, form->text(), prevCursorPos, prevAnchorPos);
3970 d->m_undoStack->push(uc);
3971}
3972
3973void Document::editFormText(int pageNumber, Okular::FormFieldText *form, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos, const QString &oldContents)
3974{
3975 QUndoCommand *uc = new EditFormTextCommand(this->d, form, pageNumber, newContents, newCursorPos, oldContents, prevCursorPos, prevAnchorPos);
3976 d->m_undoStack->push(uc);
3977}
3978
3979void Document::editFormList(int pageNumber, FormFieldChoice *form, const QList<int> &newChoices)
3980{
3981 const QList<int> prevChoices = form->currentChoices();
3982 QUndoCommand *uc = new EditFormListCommand(this->d, form, pageNumber, newChoices, prevChoices);
3983 d->m_undoStack->push(uc);
3984}
3985
3986void Document::editFormCombo(int pageNumber, FormFieldChoice *form, const QString &newText, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3987{
3988 QString prevText;
3989 if (form->currentChoices().isEmpty()) {
3990 prevText = form->editChoice();
3991 } else {
3992 prevText = form->choices().at(form->currentChoices().constFirst());
3993 }
3994
3995 QUndoCommand *uc = new EditFormComboCommand(this->d, form, pageNumber, newText, newCursorPos, prevText, prevCursorPos, prevAnchorPos);
3996 d->m_undoStack->push(uc);
3997}
3998
3999void Document::editFormButtons(int pageNumber, const QList<FormFieldButton *> &formButtons, const QList<bool> &newButtonStates)
4000{
4001 QUndoCommand *uc = new EditFormButtonsCommand(this->d, pageNumber, formButtons, newButtonStates);
4002 d->m_undoStack->push(uc);
4003}
4004
4006{
4007 const int numOfPages = pages();
4008 for (int i = currentPage(); i >= 0; i--) {
4009 d->refreshPixmaps(i);
4010 }
4011 for (int i = currentPage() + 1; i < numOfPages; i++) {
4012 d->refreshPixmaps(i);
4013 }
4014}
4015
4017{
4018 return d->m_bookmarkManager;
4019}
4020
4022{
4023 QList<int> list;
4024 uint docPages = pages();
4025
4026 // pages are 0-indexed internally, but 1-indexed externally
4027 for (uint i = 0; i < docPages; i++) {
4028 if (bookmarkManager()->isBookmarked(i)) {
4029 list << i + 1;
4030 }
4031 }
4032 return list;
4033}
4034
4036{
4037 // Code formerly in Part::slotPrint()
4038 // range detecting
4039 QString range;
4040 uint docPages = pages();
4041 int startId = -1;
4042 int endId = -1;
4043
4044 for (uint i = 0; i < docPages; ++i) {
4045 if (bookmarkManager()->isBookmarked(i)) {
4046 if (startId < 0) {
4047 startId = i;
4048 }
4049 if (endId < 0) {
4050 endId = startId;
4051 } else {
4052 ++endId;
4053 }
4054 } else if (startId >= 0 && endId >= 0) {
4055 if (!range.isEmpty()) {
4056 range += QLatin1Char(',');
4057 }
4058
4059 if (endId - startId > 0) {
4060 range += QStringLiteral("%1-%2").arg(startId + 1).arg(endId + 1);
4061 } else {
4062 range += QString::number(startId + 1);
4063 }
4064 startId = -1;
4065 endId = -1;
4066 }
4067 }
4068 if (startId >= 0 && endId >= 0) {
4069 if (!range.isEmpty()) {
4070 range += QLatin1Char(',');
4071 }
4072
4073 if (endId - startId > 0) {
4074 range += QStringLiteral("%1-%2").arg(startId + 1).arg(endId + 1);
4075 } else {
4076 range += QString::number(startId + 1);
4077 }
4078 }
4079 return range;
4080}
4081
4082struct ExecuteNextActionsHelper : public QObject, private DocumentObserver {
4083 Q_OBJECT
4084public:
4085 explicit ExecuteNextActionsHelper(Document *doc)
4086 : m_doc(doc)
4087 {
4088 doc->addObserver(this);
4089 connect(doc, &Document::aboutToClose, this, [this] { b = false; });
4090 }
4091
4092 ~ExecuteNextActionsHelper() override
4093 {
4094 m_doc->removeObserver(this);
4095 }
4096
4097 void notifySetup(const QVector<Okular::Page *> & /*pages*/, int setupFlags) override
4098 {
4099 if (setupFlags == DocumentChanged || setupFlags == UrlChanged) {
4100 b = false;
4101 }
4102 }
4103
4104 bool shouldExecuteNextAction() const
4105 {
4106 return b;
4107 }
4108
4109private:
4110 Document *const m_doc;
4111 bool b = true;
4112};
4113
4115{
4116 if (!action) {
4117 return;
4118 }
4119
4120 // Don't execute next actions if the action itself caused the closing of the document
4121 const ExecuteNextActionsHelper executeNextActionsHelper(this);
4122
4123 switch (action->actionType()) {
4124 case Action::Goto: {
4125 const GotoAction *go = static_cast<const GotoAction *>(action);
4126 d->m_nextDocumentViewport = go->destViewport();
4127 d->m_nextDocumentDestination = go->destinationName();
4128
4129 // Explanation of why d->m_nextDocumentViewport is needed:
4130 // all openRelativeFile does is launch a signal telling we
4131 // want to open another URL, the problem is that when the file is
4132 // non local, the loading is done asynchronously so you can't
4133 // do a setViewport after the if as it was because you are doing the setViewport
4134 // on the old file and when the new arrives there is no setViewport for it and
4135 // it does not show anything
4136
4137 // first open filename if link is pointing outside this document
4138 const QString filename = go->fileName();
4139 if (go->isExternal() && !d->openRelativeFile(filename)) {
4140 qCWarning(OkularCoreDebug).nospace() << "Action: Error opening '" << filename << "'.";
4141 break;
4142 } else {
4143 const DocumentViewport nextViewport = d->nextDocumentViewport();
4144 // skip local links that point to nowhere (broken ones)
4145 if (!nextViewport.isValid()) {
4146 break;
4147 }
4148
4149 setViewport(nextViewport, nullptr, true);
4150 d->m_nextDocumentViewport = DocumentViewport();
4151 d->m_nextDocumentDestination = QString();
4152 }
4153
4154 } break;
4155
4156 case Action::Execute: {
4157 const ExecuteAction *exe = static_cast<const ExecuteAction *>(action);
4158 const QString fileName = exe->fileName();
4159 if (fileName.endsWith(QLatin1String(".pdf"), Qt::CaseInsensitive)) {
4160 d->openRelativeFile(fileName);
4161 break;
4162 }
4163
4164 // Albert: the only pdf i have that has that kind of link don't define
4165 // an application and use the fileName as the file to open
4166 QUrl url = d->giveAbsoluteUrl(fileName);
4167 QMimeDatabase db;
4168 QMimeType mime = db.mimeTypeForUrl(url);
4169 // Check executables
4170 if (KIO::OpenUrlJob::isExecutableFile(url, mime.name())) {
4171 // Don't have any pdf that uses this code path, just a guess on how it should work
4172 if (!exe->parameters().isEmpty()) {
4173 url = d->giveAbsoluteUrl(exe->parameters());
4174 mime = db.mimeTypeForUrl(url);
4175
4176 if (KIO::OpenUrlJob::isExecutableFile(url, mime.name())) {
4177 // this case is a link pointing to an executable with a parameter
4178 // that also is an executable, possibly a hand-crafted pdf
4179 Q_EMIT error(i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that."), -1);
4180 break;
4181 }
4182 } else {
4183 // this case is a link pointing to an executable with no parameters
4184 // core developers find unacceptable executing it even after asking the user
4185 Q_EMIT error(i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that."), -1);
4186 break;
4187 }
4188 }
4189
4190 KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mime.name());
4192 job->start();
4193 connect(job, &KIO::OpenUrlJob::result, this, [this, mime](KJob *job) {
4194 if (job->error()) {
4195 Q_EMIT error(i18n("No application found for opening file of mimetype %1.", mime.name()), -1);
4196 }
4197 });
4198 } break;
4199
4200 case Action::DocAction: {
4201 const DocumentAction *docaction = static_cast<const DocumentAction *>(action);
4202 switch (docaction->documentActionType()) {
4204 setViewportPage(0);
4205 break;
4207 if ((*d->m_viewportIterator).pageNumber > 0) {
4208 setViewportPage((*d->m_viewportIterator).pageNumber - 1);
4209 }
4210 break;
4212 if ((*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1) {
4213 setViewportPage((*d->m_viewportIterator).pageNumber + 1);
4214 }
4215 break;
4217 setViewportPage(d->m_pagesVector.count() - 1);
4218 break;
4221 break;
4224 break;
4226 Q_EMIT quit();
4227 break;
4230 break;
4233 break;
4235 Q_EMIT linkFind();
4236 break;
4239 break;
4241 Q_EMIT close();
4242 break;
4245 break;
4248 break;
4249 }
4250 } break;
4251
4252 case Action::Browse: {
4253 const BrowseAction *browse = static_cast<const BrowseAction *>(action);
4254 QString lilySource;
4255 int lilyRow = 0, lilyCol = 0;
4256 // if the url is a mailto one, invoke mailer
4257 if (browse->url().scheme() == QLatin1String("mailto")) {
4258 QDesktopServices::openUrl(browse->url());
4259 } else if (extractLilyPondSourceReference(browse->url(), &lilySource, &lilyRow, &lilyCol)) {
4260 const SourceReference ref(lilySource, lilyRow, lilyCol);
4262 } else {
4263 const QUrl url = browse->url();
4264
4265 // fix for #100366, documents with relative links that are the form of http:foo.pdf
4266 if ((url.scheme() == QLatin1String("http")) && url.host().isEmpty() && url.fileName().endsWith(QLatin1String("pdf"))) {
4267 d->openRelativeFile(url.fileName());
4268 break;
4269 }
4270
4271 // handle documents with relative path
4272 QUrl realUrl;
4273 if (d->m_url.isValid()) {
4274 realUrl = KIO::upUrl(d->m_url).resolved(url);
4275 } else if (!url.isRelative()) {
4276 realUrl = url;
4277 }
4278 if (realUrl.isValid()) {
4279 auto *job = new KIO::OpenUrlJob(realUrl);
4280 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->m_widget.data()));
4281 job->start();
4282 }
4283 }
4284 } break;
4285
4286 case Action::Sound: {
4287 const SoundAction *linksound = static_cast<const SoundAction *>(action);
4288 AudioPlayer::instance()->playSound(linksound->sound(), linksound);
4289 } break;
4290
4291 case Action::Script: {
4292 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4293 if (!d->m_scripter) {
4294 d->m_scripter = new Scripter(d);
4295 }
4296 d->m_scripter->execute(linkscript->scriptType(), linkscript->script());
4297 } break;
4298
4299 case Action::Movie:
4300 Q_EMIT processMovieAction(static_cast<const MovieAction *>(action));
4301 break;
4302 case Action::Rendition: {
4303 const RenditionAction *linkrendition = static_cast<const RenditionAction *>(action);
4304 if (!linkrendition->script().isEmpty()) {
4305 if (!d->m_scripter) {
4306 d->m_scripter = new Scripter(d);
4307 }
4308 d->m_scripter->execute(linkrendition->scriptType(), linkrendition->script());
4309 }
4310
4311 Q_EMIT processRenditionAction(static_cast<const RenditionAction *>(action));
4312 } break;
4313 case Action::BackendOpaque: {
4314 d->m_generator->opaqueAction(static_cast<const BackendOpaqueAction *>(action));
4315 } break;
4316 }
4317
4318 if (executeNextActionsHelper.shouldExecuteNextAction()) {
4319 const QVector<Action *> nextActions = action->nextActions();
4320 for (const Action *a : nextActions) {
4321 processAction(a);
4322 }
4323 }
4324}
4325
4327{
4328 processFormatAction(action, static_cast<FormField *>(fft));
4329}
4330
4332{
4333 if (action->actionType() != Action::Script) {
4334 qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for formatting.";
4335 return;
4336 }
4337
4338 // Lookup the page of the FormFieldText
4339 int foundPage = d->findFieldPageNumber(ff);
4340
4341 if (foundPage == -1) {
4342 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4343 return;
4344 }
4345
4346 const QString unformattedText = ff->value().toString();
4347
4348 std::shared_ptr<Event> event = Event::createFormatEvent(ff, d->m_pagesVector[foundPage]);
4349
4350 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4351
4352 d->executeScriptEvent(event, linkscript);
4353
4354 const QString formattedText = event->value().toString();
4355 ff->commitFormattedValue(formattedText);
4356 if (formattedText != unformattedText) {
4357 // We set the formattedText, because when we call refreshFormWidget
4358 // It will set the QLineEdit to this formattedText
4359 ff->setValue(QVariant(formattedText));
4360 ff->setAppearanceValue(QVariant(formattedText));
4362 d->refreshPixmaps(foundPage);
4363 // Then we make the form have the unformatted text, to use
4364 // in calculations and other things
4365 ff->setValue(QVariant(unformattedText));
4367 // When the field was calculated we need to refresh even
4368 // if the format script changed nothing. e.g. on error.
4369 // This is because the recalculateForms function delegated
4370 // the responsiblity for the refresh to us.
4372 d->refreshPixmaps(foundPage);
4373 }
4374}
4375
4376QString DocumentPrivate::evaluateKeystrokeEventChange(const QString &oldVal, const QString &newVal, int selStart, int selEnd)
4377{
4378 /*
4379 The change needs to be evaluated here in accordance with code points.
4380 selStart and selEnd parameters passed to this method should be been adjusted accordingly.
4381
4382 Since QString methods work in terms of code units, we convert the strings to UTF-32.
4383 */
4384 std::u32string oldUcs4 = oldVal.toStdU32String();
4385 std::u32string newUcs4 = newVal.toStdU32String();
4386 if (selStart < 0 || selEnd < 0 || (selEnd - selStart) + (static_cast<int>(newUcs4.size()) - static_cast<int>(oldUcs4.size())) < 0) {
4387 // Prevent Okular from crashing if incorrect parameters are passed or some bug causes incorrect calculation
4388 return {};
4389 }
4390 const size_t changeLength = (selEnd - selStart) + (newUcs4.size() - oldUcs4.size());
4391 auto subview = std::u32string_view {newUcs4}.substr(selStart, changeLength);
4392 if (subview.empty()) {
4393 // If subview is empty (in scenarios when selStart is at end and changeLength is non-zero) fromUcs4 returns \u0000.
4394 // This should not happen, but just a guard.
4395 return {};
4396 }
4397 return QString::fromUcs4(subview.data(), changeLength);
4398}
4399
4400void Document::processKeystrokeAction(const Action *action, Okular::FormField *ff, const QVariant &newValue, int prevCursorPos, int prevAnchorPos)
4401{
4402 if (action->actionType() != Action::Script) {
4403 qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for keystroke.";
4404 return;
4405 }
4406 // Lookup the page of the FormFieldText
4407 int foundPage = d->findFieldPageNumber(ff);
4408
4409 if (foundPage == -1) {
4410 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4411 return;
4412 }
4413
4414 std::shared_ptr<Event> event = Event::createKeystrokeEvent(ff, d->m_pagesVector[foundPage]);
4415
4416 /* Set the selStart and selEnd event properties
4417
4418 QString using UTF-16 counts a code point as made up of 1 or 2 16-bit code units.
4419
4420 When encoded using 2 code units, the units are referred to as surrogate pairs.
4421 selectionStart() and selectionEnd() methods evaluate prevCursorPos and prevAnchorPos based on code units during selection.
4422
4423 While this unit-based evaulation is suitable for detecting changes, for providing consistency with Adobe Reader for values of selStart and selEnd,
4424 it would be best to evaluate in terms of code points rather than the code units.
4425
4426 To correct the values of selStart and selEnd accordingly, we iterate over the code units. If a surrogate pair is encountered, then selStart and
4427 selEnd are accordingly decremented.
4428 */
4429 int selStart = std::min(prevCursorPos, prevAnchorPos);
4430 int selEnd = std::max(prevCursorPos, prevAnchorPos);
4431 int codeUnit;
4432 int initialSelStart = selStart;
4433 int initialSelEnd = selEnd;
4434 QString inputString = ff->value().toString();
4435 for (codeUnit = 0; codeUnit < initialSelStart && codeUnit < inputString.size(); codeUnit++) {
4436 if (inputString.at(codeUnit).isHighSurrogate()) {
4437 // skip the low surrogate and decrement selStart and selEnd
4438 codeUnit++;
4439 selStart--;
4440 selEnd--;
4441 }
4442 }
4443 for (; codeUnit < initialSelEnd && codeUnit < inputString.size(); codeUnit++) {
4444 if (inputString.at(codeUnit).isHighSurrogate()) {
4445 // skip the low surrogate and decrement selEnd
4446 codeUnit++;
4447 selEnd--;
4448 }
4449 }
4450 std::u32string oldUcs4 = inputString.toStdU32String();
4451 std::u32string newUcs4 = newValue.toString().toStdU32String();
4452 // It is necessary to count size in terms of code points rather than code units for deletion.
4453 if (oldUcs4.size() - newUcs4.size() == 1 && selStart == selEnd) {
4454 // consider a one character removal as selection of that character and then its removal.
4455 selStart--;
4456 }
4457 event->setSelStart(selStart);
4458 event->setSelEnd(selEnd);
4459 // Use the corrected selStart and selEnd for evaluating the change.
4460 event->setChange(DocumentPrivate::evaluateKeystrokeEventChange(inputString, newValue.toString(), selStart, selEnd));
4461 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4462
4463 d->executeScriptEvent(event, linkscript);
4464
4465 if (event->returnCode()) {
4466 ff->setValue(newValue);
4467 } else {
4469 }
4470}
4471
4473{
4474 // use -1 as default
4475 processKeystrokeAction(action, fft, newValue, -1, -1);
4476}
4477
4479{
4480 bool returnCode = false;
4481 processKeystrokeCommitAction(action, fft, returnCode);
4482}
4483
4484void Document::processKeystrokeCommitAction(const Action *action, Okular::FormField *ff, bool &returnCode)
4485{
4486 if (action->actionType() != Action::Script) {
4487 qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for keystroke.";
4488 return;
4489 }
4490 // Lookup the page of the FormFieldText
4491 int foundPage = d->findFieldPageNumber(ff);
4492
4493 if (foundPage == -1) {
4494 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4495 return;
4496 }
4497
4498 std::shared_ptr<Event> event = Event::createKeystrokeEvent(ff, d->m_pagesVector[foundPage]);
4499 event->setWillCommit(true);
4500
4501 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4502
4503 d->executeScriptEvent(event, linkscript);
4504
4505 if (!event->returnCode()) {
4508 ff->setValue(QVariant(ff->committedValue()));
4509 } else {
4510 ff->setValue(QVariant(event->value().toString()));
4512 }
4513 returnCode = event->returnCode();
4514}
4515
4517{
4518 if (!action || action->actionType() != Action::Script) {
4519 return;
4520 }
4521
4522 // Lookup the page of the FormFieldText
4523 int foundPage = d->findFieldPageNumber(field);
4524
4525 if (foundPage == -1) {
4526 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4527 return;
4528 }
4529
4530 std::shared_ptr<Event> event = Event::createFormFocusEvent(field, d->m_pagesVector[foundPage]);
4531
4532 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4533
4534 d->executeScriptEvent(event, linkscript);
4535}
4536
4537void Document::processValidateAction(const Action *action, Okular::FormFieldText *fft, bool &returnCode)
4538{
4539 processValidateAction(action, static_cast<FormField *>(fft), returnCode);
4540}
4541
4542void Document::processValidateAction(const Action *action, Okular::FormField *ff, bool &returnCode)
4543{
4544 if (!action || action->actionType() != Action::Script) {
4545 return;
4546 }
4547
4548 // Lookup the page of the FormFieldText
4549 int foundPage = d->findFieldPageNumber(ff);
4550
4551 if (foundPage == -1) {
4552 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4553 return;
4554 }
4555
4556 std::shared_ptr<Event> event = Event::createFormValidateEvent(ff, d->m_pagesVector[foundPage]);
4557
4558 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4559
4560 d->executeScriptEvent(event, linkscript);
4561 if (!event->returnCode()) {
4564 ff->setValue(QVariant(ff->committedValue()));
4565 } else {
4566 ff->setValue(QVariant(event->value().toString()));
4568 }
4569 returnCode = event->returnCode();
4570}
4571
4573{
4574 if (ff->value().toString() == ff->committedValue()) {
4577 ff->setValue(QVariant(ff->committedValue()));
4578 return;
4579 }
4580
4581 bool returnCode = true;
4584 }
4585
4587 if (returnCode) {
4588 processValidateAction(action, ff, returnCode);
4589 }
4590 }
4591
4592 if (!returnCode) {
4593 return;
4594 } else {
4595 ff->commitValue(ff->value().toString());
4596 }
4597
4598 // TODO add calculation script here
4599
4601 processFormatAction(action, ff);
4602 }
4603}
4604
4606{
4607 if (!action || action->actionType() != Action::Script) {
4608 return;
4609 }
4610
4611 Event::EventType eventType = Okular::Event::UnknownEvent;
4612
4613 switch (type) {
4614 case Document::CloseDocument:
4615 eventType = Okular::Event::DocWillClose;
4616 break;
4617 case Document::SaveDocumentStart:
4618 eventType = Okular::Event::DocWillSave;
4619 break;
4620 case Document::SaveDocumentFinish:
4621 eventType = Okular::Event::DocDidSave;
4622 break;
4623 case Document::PrintDocumentStart:
4624 eventType = Okular::Event::DocWillPrint;
4625 break;
4626 case Document::PrintDocumentFinish:
4627 eventType = Okular::Event::DocDidPrint;
4628 break;
4629 }
4630
4631 std::shared_ptr<Event> event = Event::createDocEvent(eventType);
4632
4633 const ScriptAction *linkScript = static_cast<const ScriptAction *>(action);
4634
4635 d->executeScriptEvent(event, linkScript);
4636}
4637
4639{
4640 if (!action || action->actionType() != Action::Script) {
4641 return;
4642 }
4643
4644 // Lookup the page of the FormFieldText
4645 int foundPage = d->findFieldPageNumber(ff);
4646
4647 if (foundPage == -1) {
4648 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4649 return;
4650 }
4651
4652 std::shared_ptr<Event> event = Event::createFieldMouseUpEvent(ff, d->m_pagesVector[foundPage]);
4653
4654 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4655
4656 d->executeScriptEvent(event, linkscript);
4657}
4658
4660{
4661 if (!ref) {
4662 return;
4663 }
4664
4665 const QUrl url = d->giveAbsoluteUrl(ref->fileName());
4666 if (!url.isLocalFile()) {
4667 qCDebug(OkularCoreDebug) << url.url() << "is not a local file.";
4668 return;
4669 }
4670
4671 const QString absFileName = url.toLocalFile();
4672 if (!QFile::exists(absFileName)) {
4673 qCDebug(OkularCoreDebug) << "No such file:" << absFileName;
4674 return;
4675 }
4676
4677 bool handled = false;
4678 Q_EMIT sourceReferenceActivated(absFileName, ref->row(), ref->column(), &handled);
4679 if (handled) {
4680 return;
4681 }
4682
4683 static QHash<int, QString> editors;
4684 // init the editors table if empty (on first run, usually)
4685 if (editors.isEmpty()) {
4686 editors = buildEditorsMap();
4687 }
4688
4689 // prefer the editor from the command line
4690 QString p = d->editorCommandOverride;
4691 if (p.isEmpty()) {
4692 QHash<int, QString>::const_iterator it = editors.constFind(SettingsCore::externalEditor());
4693 if (it != editors.constEnd()) {
4694 p = *it;
4695 } else {
4696 p = SettingsCore::externalEditorCommand();
4697 }
4698 }
4699 // custom editor not yet configured
4700 if (p.isEmpty()) {
4701 return;
4702 }
4703
4704 // manually append the %f placeholder if not specified
4705 if (p.indexOf(QLatin1String("%f")) == -1) {
4706 p.append(QLatin1String(" %f"));
4707 }
4708
4709 // replacing the placeholders
4711 map.insert(QLatin1Char('f'), absFileName);
4712 map.insert(QLatin1Char('c'), QString::number(ref->column()));
4713 map.insert(QLatin1Char('l'), QString::number(ref->row()));
4715 if (cmd.isEmpty()) {
4716 return;
4717 }
4718 QStringList args = KShell::splitArgs(cmd);
4719 if (args.isEmpty()) {
4720 return;
4721 }
4722
4723 const QString prog = args.takeFirst();
4724 // Make sure prog is in PATH and not just in the CWD
4725 const QString progFullPath = QStandardPaths::findExecutable(prog);
4726 if (progFullPath.isEmpty()) {
4727 return;
4728 }
4729
4730 KProcess::startDetached(progFullPath, args);
4731}
4732
4733const SourceReference *Document::dynamicSourceReference(int pageNr, double absX, double absY)
4734{
4735 if (!d->m_synctex_scanner) {
4736 return nullptr;
4737 }
4738
4739 const QSizeF dpi = d->m_generator->dpi();
4740
4741 if (synctex_edit_query(d->m_synctex_scanner, pageNr + 1, absX * 72. / dpi.width(), absY * 72. / dpi.height()) > 0) {
4742 synctex_node_p node;
4743 // TODO what should we do if there is really more than one node?
4744 while ((node = synctex_scanner_next_result(d->m_synctex_scanner))) {
4745 int line = synctex_node_line(node);
4746 int col = synctex_node_column(node);
4747 // column extraction does not seem to be implemented in synctex so far. set the SourceReference default value.
4748 if (col == -1) {
4749 col = 0;
4750 }
4751 const char *name = synctex_scanner_get_name(d->m_synctex_scanner, synctex_node_tag(node));
4752
4753 return new Okular::SourceReference(QFile::decodeName(name), line, col);
4754 }
4755 }
4756 return nullptr;
4757}
4758
4760{
4761 if (d->m_generator) {
4762 if (d->m_generator->hasFeature(Generator::PrintNative)) {
4763 return NativePrinting;
4764 }
4765
4766#ifndef Q_OS_WIN
4767 if (d->m_generator->hasFeature(Generator::PrintPostscript)) {
4768 return PostscriptPrinting;
4769 }
4770#endif
4771 }
4772
4773 return NoPrinting;
4774}
4775
4777{
4778 return d->m_generator ? d->m_generator->hasFeature(Generator::PrintToFile) : false;
4779}
4780
4782{
4783 if (const Okular::Action *action = d->m_generator->additionalDocumentAction(PrintDocumentStart)) {
4784 processDocumentAction(action, PrintDocumentStart);
4785 }
4786 const Document::PrintError printError = d->m_generator ? d->m_generator->print(printer) : Document::UnknownPrintError;
4787 if (printError == Document::NoPrintError) {
4788 if (const Okular::Action *action = d->m_generator->additionalDocumentAction(PrintDocumentFinish)) {
4789 processDocumentAction(action, PrintDocumentFinish);
4790 }
4791 }
4792 return printError;
4793}
4794
4796{
4797 switch (error) {
4798 case TemporaryFileOpenPrintError:
4799 return i18n("Could not open a temporary file");
4800 case FileConversionPrintError:
4801 return i18n("Print conversion failed");
4802 case PrintingProcessCrashPrintError:
4803 return i18n("Printing process crashed");
4804 case PrintingProcessStartPrintError:
4805 return i18n("Printing process could not start");
4806 case PrintToFilePrintError:
4807 return i18n("Printing to file failed");
4808 case InvalidPrinterStatePrintError:
4809 return i18n("Printer was in invalid state");
4810 case UnableToFindFilePrintError:
4811 return i18n("Unable to find file to print");
4812 case NoFileToPrintError:
4813 return i18n("There was no file to print");
4814 case NoBinaryToPrintError:
4815 return i18n("Could not find a suitable binary for printing. Make sure CUPS lpr binary is available");
4816 case InvalidPageSizePrintError:
4817 return i18n("The page print size is invalid");
4818 case NoPrintError:
4819 return QString();
4820 case UnknownPrintError:
4821 return QString();
4822 }
4823
4824 return QString();
4825}
4826
4828{
4829 if (d->m_generator) {
4831 return iface ? iface->printConfigurationWidget() : nullptr;
4832 } else {
4833 return nullptr;
4834 }
4835}
4836
4838{
4839 if (!dialog) {
4840 return;
4841 }
4842
4843 // We know it's a BackendConfigDialog, but check anyway
4844 BackendConfigDialog *bcd = dynamic_cast<BackendConfigDialog *>(dialog);
4845 if (!bcd) {
4846 return;
4847 }
4848
4849 // ensure that we have all the generators with settings loaded
4850 QVector<KPluginMetaData> offers = DocumentPrivate::configurableGenerators();
4851 d->loadServiceList(offers);
4852
4853 // We want the generators to be sorted by name so let's fill in a QMap
4854 // this sorts by internal id which is not awesome, but at least the sorting
4855 // is stable between runs that before it wasn't
4856 QMap<QString, GeneratorInfo> sortedGenerators;
4857 QHash<QString, GeneratorInfo>::iterator it = d->m_loadedGenerators.begin();
4858 QHash<QString, GeneratorInfo>::iterator itEnd = d->m_loadedGenerators.end();
4859 for (; it != itEnd; ++it) {
4860 sortedGenerators.insert(it.key(), it.value());
4861 }
4862
4863 bool pagesAdded = false;
4864 QMap<QString, GeneratorInfo>::iterator sit = sortedGenerators.begin();
4865 QMap<QString, GeneratorInfo>::iterator sitEnd = sortedGenerators.end();
4866 for (; sit != sitEnd; ++sit) {
4867 Okular::ConfigInterface *iface = d->generatorConfig(sit.value());
4868 if (iface) {
4869 iface->addPages(dialog);
4870 pagesAdded = true;
4871
4872 if (sit.value().generator == d->m_generator) {
4873 const int rowCount = bcd->thePageWidget()->model()->rowCount();
4874 KPageView *view = bcd->thePageWidget();
4875 view->setCurrentPage(view->model()->index(rowCount - 1, 0));
4876 }
4877 }
4878 }
4879 if (pagesAdded) {
4880 connect(dialog, &KConfigDialog::settingsChanged, this, [this] { d->slotGeneratorConfigChanged(); });
4881 }
4882}
4883
4884QVector<KPluginMetaData> DocumentPrivate::configurableGenerators()
4885{
4886 const QVector<KPluginMetaData> available = availableGenerators();
4888 for (const KPluginMetaData &md : available) {
4889 if (md.rawData().value(QStringLiteral("X-KDE-okularHasInternalSettings")).toBool()) {
4890 result << md;
4891 }
4892 }
4893 return result;
4894}
4895
4897{
4898 if (!d->m_generator) {
4899 return KPluginMetaData();
4900 }
4901
4902 auto genIt = d->m_loadedGenerators.constFind(d->m_generatorName);
4903 Q_ASSERT(genIt != d->m_loadedGenerators.constEnd());
4904 return genIt.value().metadata;
4905}
4906
4908{
4909 return DocumentPrivate::configurableGenerators().size();
4910}
4911
4913{
4914 // TODO: make it a static member of DocumentPrivate?
4915 QStringList result = d->m_supportedMimeTypes;
4916 if (result.isEmpty()) {
4917 const QVector<KPluginMetaData> available = DocumentPrivate::availableGenerators();
4918 for (const KPluginMetaData &md : available) {
4919 result << md.mimeTypes();
4920 }
4921
4922 // Remove duplicate mimetypes represented by different names
4923 QMimeDatabase mimeDatabase;
4924 QSet<QMimeType> uniqueMimetypes;
4925 for (const QString &mimeName : std::as_const(result)) {
4926 uniqueMimetypes.insert(mimeDatabase.mimeTypeForName(mimeName));
4927 }
4928 result.clear();
4929 for (const QMimeType &mimeType : uniqueMimetypes) {
4930 result.append(mimeType.name());
4931 }
4932
4933 // Add the Okular archive mimetype
4934 result << QStringLiteral("application/vnd.kde.okular-archive");
4935
4936 // Sorting by mimetype name doesn't make a ton of sense,
4937 // but ensures that the list is ordered the same way every time
4938 std::sort(result.begin(), result.end());
4939
4940 d->m_supportedMimeTypes = result;
4941 }
4942 return result;
4943}
4944
4946{
4947 if (!d->m_generator) {
4948 return false;
4949 }
4950
4951 return d->m_generator->hasFeature(Generator::SwapBackingFile);
4952}
4953
4954bool Document::swapBackingFile(const QString &newFileName, const QUrl &url)
4955{
4956 if (!d->m_generator) {
4957 return false;
4958 }
4959
4960 if (!d->m_generator->hasFeature(Generator::SwapBackingFile)) {
4961 return false;
4962 }
4963
4964 // Save metadata about the file we're about to close
4965 d->saveDocumentInfo();
4966
4967 d->clearAndWaitForRequests();
4968
4969 qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName;
4970 QVector<Page *> newPagesVector;
4971 Generator::SwapBackingFileResult result = d->m_generator->swapBackingFile(newFileName, newPagesVector);
4972 if (result != Generator::SwapBackingFileError) {
4973 QList<ObjectRect *> rectsToDelete;
4974 QList<Annotation *> annotationsToDelete;
4975 QSet<PagePrivate *> pagePrivatesToDelete;
4976
4977 if (result == Generator::SwapBackingFileReloadInternalData) {
4978 // Here we need to replace everything that the old generator
4979 // had created with what the new one has without making it look like
4980 // we have actually closed and opened the file again
4981
4982 // Simple sanity check
4983 if (newPagesVector.count() != d->m_pagesVector.count()) {
4984 return false;
4985 }
4986
4987 // Update the undo stack contents
4988 for (int i = 0; i < d->m_undoStack->count(); ++i) {
4989 // Trust me on the const_cast ^_^
4990 QUndoCommand *uc = const_cast<QUndoCommand *>(d->m_undoStack->command(i));
4991 if (OkularUndoCommand *ouc = dynamic_cast<OkularUndoCommand *>(uc)) {
4992 const bool success = ouc->refreshInternalPageReferences(newPagesVector);
4993 if (!success) {
4994 qWarning() << "Document::swapBackingFile: refreshInternalPageReferences failed" << ouc;
4995 return false;
4996 }
4997 } else {
4998 qWarning() << "Document::swapBackingFile: Unhandled undo command" << uc;
4999 return false;
5000 }
5001 }
5002
5003 for (int i = 0; i < d->m_pagesVector.count(); ++i) {
5004 // switch the PagePrivate* from newPage to oldPage
5005 // this way everyone still holding Page* doesn't get
5006 // disturbed by it
5007 Page *oldPage = d->m_pagesVector[i];
5008 Page *newPage = newPagesVector[i];
5009 newPage->d->adoptGeneratedContents(oldPage->d);
5010
5011 pagePrivatesToDelete << oldPage->d;
5012 oldPage->d = newPage->d;
5013 oldPage->d->m_page = oldPage;
5014 oldPage->d->m_doc = d;
5015 newPage->d = nullptr;
5016
5017 annotationsToDelete << oldPage->m_annotations;
5018 rectsToDelete << oldPage->m_rects;
5019 oldPage->m_annotations = newPage->m_annotations;
5020 oldPage->m_rects = newPage->m_rects;
5021 }
5022 qDeleteAll(newPagesVector);
5023 }
5024
5025 d->m_url = url;
5026 d->m_docFileName = newFileName;
5027 d->updateMetadataXmlNameAndDocSize();
5028 d->m_bookmarkManager->setUrl(d->m_url);
5029 d->m_documentInfo = DocumentInfo();
5030 d->m_documentInfoAskedKeys.clear();
5031
5032 if (d->m_synctex_scanner) {
5033 synctex_scanner_free(d->m_synctex_scanner);
5034 d->m_synctex_scanner = synctex_scanner_new_with_output_file(QFile::encodeName(newFileName).constData(), nullptr, 1);
5035 if (!d->m_synctex_scanner && QFile::exists(newFileName + QLatin1String("sync"))) {
5036 d->loadSyncFile(newFileName);
5037 }
5038 }
5039
5040 foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::UrlChanged));
5041
5042 qDeleteAll(annotationsToDelete);
5043 qDeleteAll(rectsToDelete);
5044 qDeleteAll(pagePrivatesToDelete);
5045
5046 return true;
5047 } else {
5048 return false;
5049 }
5050}
5051
5052bool Document::swapBackingFileArchive(const QString &newFileName, const QUrl &url)
5053{
5054 qCDebug(OkularCoreDebug) << "Swapping backing archive to" << newFileName;
5055
5056 ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive(newFileName);
5057 if (!newArchive) {
5058 return false;
5059 }
5060
5061 const QString tempFileName = newArchive->document.fileName();
5062
5063 const bool success = swapBackingFile(tempFileName, url);
5064
5065 if (success) {
5066 delete d->m_archiveData;
5067 d->m_archiveData = newArchive;
5068 }
5069
5070 return success;
5071}
5072
5074{
5075 if (clean) {
5076 d->m_undoStack->setClean();
5077 } else {
5078 d->m_undoStack->resetClean();
5079 }
5080}
5081
5082bool Document::isHistoryClean() const
5083{
5084 return d->m_undoStack->isClean();
5085}
5086
5088{
5089 if (!d->m_generator) {
5090 return false;
5091 }
5092 Q_ASSERT(!d->m_generatorName.isEmpty());
5093
5094 QHash<QString, GeneratorInfo>::iterator genIt = d->m_loadedGenerators.find(d->m_generatorName);
5095 Q_ASSERT(genIt != d->m_loadedGenerators.end());
5096 SaveInterface *saveIface = d->generatorSave(genIt.value());
5097 if (!saveIface) {
5098 return false;
5099 }
5100
5101 return saveIface->supportsOption(SaveInterface::SaveChanges);
5102}
5103
5105{
5106 switch (cap) {
5108 /* Assume that if the generator supports saving, forms can be saved.
5109 * We have no means to actually query the generator at the moment
5110 * TODO: Add some method to query the generator in SaveInterface */
5111 return canSaveChanges();
5112
5114 return d->canAddAnnotationsNatively();
5115 }
5116
5117 return false;
5118}
5119
5120bool Document::saveChanges(const QString &fileName)
5121{
5122 QString errorText;
5123 return saveChanges(fileName, &errorText);
5124}
5125
5126bool Document::saveChanges(const QString &fileName, QString *errorText)
5127{
5128 if (!d->m_generator || fileName.isEmpty()) {
5129 return false;
5130 }
5131 Q_ASSERT(!d->m_generatorName.isEmpty());
5132
5133 QHash<QString, GeneratorInfo>::iterator genIt = d->m_loadedGenerators.find(d->m_generatorName);
5134 Q_ASSERT(genIt != d->m_loadedGenerators.end());
5135 SaveInterface *saveIface = d->generatorSave(genIt.value());
5136 if (!saveIface || !saveIface->supportsOption(SaveInterface::SaveChanges)) {
5137 return false;
5138 }
5139
5140 if (const Okular::Action *action = d->m_generator->additionalDocumentAction(SaveDocumentStart)) {
5141 processDocumentAction(action, SaveDocumentStart);
5142 }
5143
5144 bool success = saveIface->save(fileName, SaveInterface::SaveChanges, errorText);
5145 if (success) {
5146 if (const Okular::Action *action = d->m_generator->additionalDocumentAction(SaveDocumentFinish)) {
5147 processDocumentAction(action, SaveDocumentFinish);
5148 }
5149 }
5150 return success;
5151}
5152
5154{
5155 if (!view) {
5156 return;
5157 }
5158
5159 Document *viewDoc = view->viewDocument();
5160 if (viewDoc) {
5161 // check if already registered for this document
5162 if (viewDoc == this) {
5163 return;
5164 }
5165
5166 viewDoc->unregisterView(view);
5167 }
5168
5169 d->m_views.insert(view);
5170 view->d_func()->document = d;
5171}
5172
5174{
5175 if (!view) {
5176 return;
5177 }
5178
5179 const Document *viewDoc = view->viewDocument();
5180 if (!viewDoc || viewDoc != this) {
5181 return;
5182 }
5183
5184 view->d_func()->document = nullptr;
5185 d->m_views.remove(view);
5186}
5187
5189{
5190 if (d->m_generator) {
5191 return d->m_generator->requestFontData(font);
5192 }
5193
5194 return {};
5195}
5196
5197ArchiveData *DocumentPrivate::unpackDocumentArchive(const QString &archivePath)
5198{
5199 QMimeDatabase db;
5200 const QMimeType mime = db.mimeTypeForFile(archivePath, QMimeDatabase::MatchExtension);
5201 if (!mime.inherits(QStringLiteral("application/vnd.kde.okular-archive"))) {
5202 return nullptr;
5203 }
5204
5205 KZip okularArchive(archivePath);
5206 if (!okularArchive.open(QIODevice::ReadOnly)) {
5207 return nullptr;
5208 }
5209
5210 const KArchiveDirectory *mainDir = okularArchive.directory();
5211
5212 // Check the archive doesn't have folders, we don't create them when saving the archive
5213 // and folders mean paths and paths mean path traversal issues
5214 const QStringList mainDirEntries = mainDir->entries();
5215 for (const QString &entry : mainDirEntries) {
5216 if (mainDir->entry(entry)->isDirectory()) {
5217 qWarning() << "Warning: Found a directory inside" << archivePath << " - Okular does not create files like that so it is most probably forged.";
5218 return nullptr;
5219 }
5220 }
5221
5222 const KArchiveEntry *mainEntry = mainDir->entry(QStringLiteral("content.xml"));
5223 if (!mainEntry || !mainEntry->isFile()) {
5224 return nullptr;
5225 }
5226
5227 std::unique_ptr<QIODevice> mainEntryDevice(static_cast<const KZipFileEntry *>(mainEntry)->createDevice());
5228 QDomDocument doc;
5229 if (!doc.setContent(mainEntryDevice.get())) {
5230 return nullptr;
5231 }
5232 mainEntryDevice.reset();
5233
5234 QDomElement root = doc.documentElement();
5235 if (root.tagName() != QLatin1String("OkularArchive")) {
5236 return nullptr;
5237 }
5238
5239 QString documentFileName;
5240 QString metadataFileName;
5241 QDomElement el = root.firstChild().toElement();
5242 for (; !el.isNull(); el = el.nextSibling().toElement()) {
5243 if (el.tagName() == QLatin1String("Files")) {
5244 QDomElement fileEl = el.firstChild().toElement();
5245 for (; !fileEl.isNull(); fileEl = fileEl.nextSibling().toElement()) {
5246 if (fileEl.tagName() == QLatin1String("DocumentFileName")) {
5247 documentFileName = fileEl.text();
5248 } else if (fileEl.tagName() == QLatin1String("MetadataFileName")) {
5249 metadataFileName = fileEl.text();
5250 }
5251 }
5252 }
5253 }
5254 if (documentFileName.isEmpty()) {
5255 return nullptr;
5256 }
5257
5258 const KArchiveEntry *docEntry = mainDir->entry(documentFileName);
5259 if (!docEntry || !docEntry->isFile()) {
5260 return nullptr;
5261 }
5262
5263 std::unique_ptr<ArchiveData> archiveData(new ArchiveData());
5264 const int dotPos = documentFileName.indexOf(QLatin1Char('.'));
5265 if (dotPos != -1) {
5266 archiveData->document.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX") + documentFileName.mid(dotPos));
5267 }
5268 if (!archiveData->document.open()) {
5269 return nullptr;
5270 }
5271
5272 archiveData->originalFileName = documentFileName;
5273
5274 {
5275 std::unique_ptr<QIODevice> docEntryDevice(static_cast<const KZipFileEntry *>(docEntry)->createDevice());
5276 copyQIODevice(docEntryDevice.get(), &archiveData->document);
5277 archiveData->document.close();
5278 }
5279
5280 const KArchiveEntry *metadataEntry = mainDir->entry(metadataFileName);
5281 if (metadataEntry && metadataEntry->isFile()) {
5282 std::unique_ptr<QIODevice> metadataEntryDevice(static_cast<const KZipFileEntry *>(metadataEntry)->createDevice());
5283 archiveData->metadataFile.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX.xml"));
5284 if (archiveData->metadataFile.open()) {
5285 copyQIODevice(metadataEntryDevice.get(), &archiveData->metadataFile);
5286 archiveData->metadataFile.close();
5287 }
5288 }
5289
5290 return archiveData.release();
5291}
5292
5293Document::OpenResult Document::openDocumentArchive(const QString &docFile, const QUrl &url, const QString &password)
5294{
5295 d->m_archiveData = DocumentPrivate::unpackDocumentArchive(docFile);
5296 if (!d->m_archiveData) {
5297 return OpenError;
5298 }
5299
5300 const QString tempFileName = d->m_archiveData->document.fileName();
5301 QMimeDatabase db;
5302 const QMimeType docMime = db.mimeTypeForFile(tempFileName, QMimeDatabase::MatchExtension);
5303 const OpenResult ret = openDocument(tempFileName, url, docMime, password);
5304
5305 if (ret != OpenSuccess) {
5306 delete d->m_archiveData;
5307 d->m_archiveData = nullptr;
5308 }
5309
5310 return ret;
5311}
5312
5314{
5315 if (!d->m_generator) {
5316 return false;
5317 }
5318
5319 /* If we opened an archive, use the name of original file (eg foo.pdf)
5320 * instead of the archive's one (eg foo.okular) */
5321 QString docFileName = d->m_archiveData ? d->m_archiveData->originalFileName : d->m_url.fileName();
5322 if (docFileName == QLatin1String("-")) {
5323 return false;
5324 }
5325
5326 QString docPath = d->m_docFileName;
5327 const QFileInfo fi(docPath);
5328 if (fi.isSymLink()) {
5329 docPath = fi.symLinkTarget();
5330 }
5331
5332 KZip okularArchive(fileName);
5333 if (!okularArchive.open(QIODevice::WriteOnly)) {
5334 return false;
5335 }
5336
5337 const KUser user;
5338#ifndef Q_OS_WIN
5339 const KUserGroup userGroup(user.groupId());
5340#else
5341 const KUserGroup userGroup(QStringLiteral(""));
5342#endif
5343
5344 QDomDocument contentDoc(QStringLiteral("OkularArchive"));
5345 QDomProcessingInstruction xmlPi = contentDoc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
5346 contentDoc.appendChild(xmlPi);
5347 QDomElement root = contentDoc.createElement(QStringLiteral("OkularArchive"));
5348 contentDoc.appendChild(root);
5349
5350 QDomElement filesNode = contentDoc.createElement(QStringLiteral("Files"));
5351 root.appendChild(filesNode);
5352
5353 QDomElement fileNameNode = contentDoc.createElement(QStringLiteral("DocumentFileName"));
5354 filesNode.appendChild(fileNameNode);
5355 fileNameNode.appendChild(contentDoc.createTextNode(docFileName));
5356
5357 QDomElement metadataFileNameNode = contentDoc.createElement(QStringLiteral("MetadataFileName"));
5358 filesNode.appendChild(metadataFileNameNode);
5359 metadataFileNameNode.appendChild(contentDoc.createTextNode(QStringLiteral("metadata.xml")));
5360
5361 // If the generator can save annotations natively, do it
5362 QTemporaryFile modifiedFile;
5363 bool annotationsSavedNatively = false;
5364 bool formsSavedNatively = false;
5365 if (d->canAddAnnotationsNatively() || canSaveChanges(SaveFormsCapability)) {
5366 if (!modifiedFile.open()) {
5367 return false;
5368 }
5369
5370 const QString modifiedFileName = modifiedFile.fileName();
5371
5372 modifiedFile.close(); // We're only interested in the file name
5373
5374 QString errorText;
5375 if (saveChanges(modifiedFileName, &errorText)) {
5376 docPath = modifiedFileName; // Save this instead of the original file
5377 annotationsSavedNatively = d->canAddAnnotationsNatively();
5378 formsSavedNatively = canSaveChanges(SaveFormsCapability);
5379 } else {
5380 qCWarning(OkularCoreDebug) << "saveChanges failed: " << errorText;
5381 qCDebug(OkularCoreDebug) << "Falling back to saving a copy of the original file";
5382 }
5383 }
5384
5385 PageItems saveWhat = None;
5386 if (!annotationsSavedNatively) {
5387 saveWhat |= AnnotationPageItems;
5388 }
5389 if (!formsSavedNatively) {
5390 saveWhat |= FormFieldPageItems;
5391 }
5392
5393 QTemporaryFile metadataFile;
5394 if (!d->savePageDocumentInfo(&metadataFile, saveWhat)) {
5395 return false;
5396 }
5397
5398 const QByteArray contentDocXml = contentDoc.toByteArray();
5399 const mode_t perm = 0100644;
5400 okularArchive.writeFile(QStringLiteral("content.xml"), contentDocXml, perm, user.loginName(), userGroup.name());
5401
5402 okularArchive.addLocalFile(docPath, docFileName);
5403 okularArchive.addLocalFile(metadataFile.fileName(), QStringLiteral("metadata.xml"));
5404
5405 if (!okularArchive.close()) {
5406 return false;
5407 }
5408
5409 return true;
5410}
5411
5413{
5414 if (!d->m_archiveData) {
5415 return false;
5416 }
5417
5418 // Remove existing file, if present (QFile::copy doesn't overwrite by itself)
5419 QFile::remove(destFileName);
5420
5421 return d->m_archiveData->document.copy(destFileName);
5422}
5423
5425{
5426 int landscape, portrait;
5427
5428 // if some pages are landscape and others are not, the most common wins, as
5429 // QPrinter does not accept a per-page setting
5430 landscape = 0;
5431 portrait = 0;
5432 for (uint i = 0; i < pages(); i++) {
5433 const Okular::Page *currentPage = page(i);
5434 double width = currentPage->width();
5435 double height = currentPage->height();
5436 if (currentPage->orientation() == Okular::Rotation90 || currentPage->orientation() == Okular::Rotation270) {
5437 std::swap(width, height);
5438 }
5439 if (width > height) {
5440 landscape++;
5441 } else {
5442 portrait++;
5443 }
5444 }
5445 return (landscape > portrait) ? QPageLayout::Landscape : QPageLayout::Portrait;
5446}
5447
5449{
5450 d->m_annotationEditingEnabled = enable;
5451 foreachObserver(notifySetup(d->m_pagesVector, 0));
5452}
5453
5454void Document::walletDataForFile(const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey) const
5455{
5456 if (d->m_generator) {
5457 d->m_generator->walletDataForFile(fileName, walletName, walletFolder, walletKey);
5458 } else if (d->m_walletGenerator) {
5459 d->m_walletGenerator->walletDataForFile(fileName, walletName, walletFolder, walletKey);
5460 }
5461}
5462
5464{
5465 return d->m_docdataMigrationNeeded;
5466}
5467
5469{
5470 if (d->m_docdataMigrationNeeded) {
5471 d->m_docdataMigrationNeeded = false;
5472 foreachObserver(notifySetup(d->m_pagesVector, 0));
5473 }
5474}
5475
5477{
5478 return d->m_generator ? d->m_generator->layersModel() : nullptr;
5479}
5480
5482{
5483 return d->m_openError;
5484}
5485
5486QByteArray Document::requestSignedRevisionData(const Okular::SignatureInfo &info)
5487{
5488 QFile f(d->m_docFileName);
5489 if (!f.open(QIODevice::ReadOnly)) {
5490 Q_EMIT error(i18n("Could not open '%1'. File does not exist", d->m_docFileName), -1);
5491 return {};
5492 }
5493
5494 const QList<qint64> byteRange = info.signedRangeBounds();
5495 f.seek(byteRange.first());
5496 QByteArray data = f.read(byteRange.last() - byteRange.first());
5497 f.close();
5498
5499 return data;
5500}
5501
5502void Document::refreshPixmaps(int pageNumber)
5503{
5504 d->refreshPixmaps(pageNumber);
5505}
5506
5507void DocumentPrivate::executeScript(const QString &function)
5508{
5509 if (!m_scripter) {
5510 m_scripter = new Scripter(this);
5511 }
5512 m_scripter->execute(JavaScript, function);
5513}
5514
5515void DocumentPrivate::requestDone(PixmapRequest *req)
5516{
5517 if (!req) {
5518 return;
5519 }
5520
5521 if (!m_generator || m_closingLoop) {
5522 m_pixmapRequestsMutex.lock();
5523 m_executingPixmapRequests.remove(req);
5524 m_pixmapRequestsMutex.unlock();
5525 delete req;
5526 if (m_closingLoop) {
5527 m_closingLoop->exit();
5528 }
5529 return;
5530 }
5531
5532#ifndef NDEBUG
5533 if (!m_generator->canGeneratePixmap()) {
5534 qCDebug(OkularCoreDebug) << "requestDone with generator not in READY state.";
5535 }
5536#endif
5537
5538 if (!req->shouldAbortRender()) {
5539 // [MEM] 1.1 find and remove a previous entry for the same page and id
5540 std::list<AllocatedPixmap *>::iterator aIt = m_allocatedPixmaps.begin();
5541 std::list<AllocatedPixmap *>::iterator aEnd = m_allocatedPixmaps.end();
5542 for (; aIt != aEnd; ++aIt) {
5543 if ((*aIt)->page == req->pageNumber() && (*aIt)->observer == req->observer()) {
5544 AllocatedPixmap *p = *aIt;
5545 m_allocatedPixmaps.erase(aIt);
5546 m_allocatedPixmapsTotalMemory -= p->memory;
5547 delete p;
5548 break;
5549 }
5550 }
5551
5552 DocumentObserver *observer = req->observer();
5553 if (m_observers.contains(observer)) {
5554 // [MEM] 1.2 append memory allocation descriptor to the FIFO
5555 qulonglong memoryBytes = 0;
5556 const TilesManager *tm = req->d->tilesManager();
5557 if (tm) {
5558 memoryBytes = tm->totalMemory();
5559 } else {
5560 memoryBytes = 4 * req->width() * req->height();
5561 }
5562
5563 AllocatedPixmap *memoryPage = new AllocatedPixmap(req->observer(), req->pageNumber(), memoryBytes);
5564 m_allocatedPixmaps.push_back(memoryPage);
5565 m_allocatedPixmapsTotalMemory += memoryBytes;
5566
5567 // 2. notify an observer that its pixmap changed
5569 }
5570#ifndef NDEBUG
5571 else {
5572 qCWarning(OkularCoreDebug) << "Receiving a done request for the defunct observer" << observer;
5573 }
5574#endif
5575 }
5576
5577 // 3. delete request
5578 m_pixmapRequestsMutex.lock();
5579 m_executingPixmapRequests.remove(req);
5580 m_pixmapRequestsMutex.unlock();
5581 delete req;
5582
5583 // 4. start a new generation if some is pending
5584 m_pixmapRequestsMutex.lock();
5585 bool hasPixmaps = !m_pixmapRequestsStack.empty();
5586 m_pixmapRequestsMutex.unlock();
5587 if (hasPixmaps) {
5588 sendGeneratorPixmapRequest();
5589 }
5590}
5591
5592void DocumentPrivate::setPageBoundingBox(int page, const NormalizedRect &boundingBox)
5593{
5594 Page *kp = m_pagesVector[page];
5595 if (!m_generator || !kp) {
5596 return;
5597 }
5598
5599 if (kp->boundingBox() == boundingBox) {
5600 return;
5601 }
5602 kp->setBoundingBox(boundingBox);
5603
5604 // notify observers about the change
5605 foreachObserverD(notifyPageChanged(page, DocumentObserver::BoundingBox));
5606
5607 // TODO: For generators that generate the bbox by pixmap scanning, if the first generated pixmap is very small, the bounding box will forever be inaccurate.
5608 // TODO: Crop computation should also consider annotations, actions, etc. to make sure they're not cropped away.
5609 // TODO: Help compute bounding box for generators that create a QPixmap without a QImage, like text and plucker.
5610 // TODO: Don't compute the bounding box if no one needs it (e.g., Trim Borders is off).
5611}
5612
5613void DocumentPrivate::calculateMaxTextPages()
5614{
5615 int multipliers = qMax(1, qRound(getTotalMemory() / 536870912.0)); // 512 MB
5616 switch (SettingsCore::memoryLevel()) {
5617 case SettingsCore::EnumMemoryLevel::Low:
5618 m_maxAllocatedTextPages = multipliers * 2;
5619 break;
5620
5621 case SettingsCore::EnumMemoryLevel::Normal:
5622 m_maxAllocatedTextPages = multipliers * 50;
5623 break;
5624
5625 case SettingsCore::EnumMemoryLevel::Aggressive:
5626 m_maxAllocatedTextPages = multipliers * 250;
5627 break;
5628
5629 case SettingsCore::EnumMemoryLevel::Greedy:
5630 m_maxAllocatedTextPages = multipliers * 1250;
5631 break;
5632 }
5633}
5634
5635void DocumentPrivate::textGenerationDone(Page *page)
5636{
5637 if (!m_pageController) {
5638 return;
5639 }
5640
5641 // 1. If we reached the cache limit, delete the first text page from the fifo
5642 if (m_allocatedTextPagesFifo.size() == m_maxAllocatedTextPages) {
5643 int pageToKick = m_allocatedTextPagesFifo.takeFirst();
5644 if (pageToKick != page->number()) // this should never happen but better be safe than sorry
5645 {
5646 m_pagesVector.at(pageToKick)->setTextPage(nullptr); // deletes the textpage
5647 }
5648 }
5649
5650 // 2. Add the page to the fifo of generated text pages
5651 m_allocatedTextPagesFifo.append(page->number());
5652}
5653
5655{
5656 d->setRotationInternal(r, true);
5657}
5658
5659void DocumentPrivate::setRotationInternal(int r, bool notify)
5660{
5661 Rotation rotation = (Rotation)r;
5662 if (!m_generator || (m_rotation == rotation)) {
5663 return;
5664 }
5665
5666 // tell the pages to rotate
5667 QVector<Okular::Page *>::const_iterator pIt = m_pagesVector.constBegin();
5668 QVector<Okular::Page *>::const_iterator pEnd = m_pagesVector.constEnd();
5669 for (; pIt != pEnd; ++pIt) {
5670 (*pIt)->d->rotateAt(rotation);
5671 }
5672 if (notify) {
5673 // notify the generator that the current rotation has changed
5674 m_generator->rotationChanged(rotation, m_rotation);
5675 }
5676 // set the new rotation
5677 m_rotation = rotation;
5678
5679 if (notify) {
5680 foreachObserverD(notifySetup(m_pagesVector, DocumentObserver::NewLayoutForPages));
5681 foreachObserverD(notifyContentsCleared(DocumentObserver::Pixmap | DocumentObserver::Highlights | DocumentObserver::Annotations));
5682 }
5683 qCDebug(OkularCoreDebug) << "Rotated:" << r;
5684}
5685
5687{
5688 if (!d->m_generator || !d->m_generator->hasFeature(Generator::PageSizes)) {
5689 return;
5690 }
5691
5692 if (d->m_pageSizes.isEmpty()) {
5693 d->m_pageSizes = d->m_generator->pageSizes();
5694 }
5695 int sizeid = d->m_pageSizes.indexOf(size);
5696 if (sizeid == -1) {
5697 return;
5698 }
5699
5700 // tell the pages to change size
5701 QVector<Okular::Page *>::const_iterator pIt = d->m_pagesVector.constBegin();
5702 QVector<Okular::Page *>::const_iterator pEnd = d->m_pagesVector.constEnd();
5703 for (; pIt != pEnd; ++pIt) {
5704 (*pIt)->d->changeSize(size);
5705 }
5706 // clear 'memory allocation' descriptors
5707 qDeleteAll(d->m_allocatedPixmaps);
5708 d->m_allocatedPixmaps.clear();
5709 d->m_allocatedPixmapsTotalMemory = 0;
5710 // notify the generator that the current page size has changed
5711 d->m_generator->pageSizeChanged(size, d->m_pageSize);
5712 // set the new page size
5713 d->m_pageSize = size;
5714
5715 foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::NewLayoutForPages));
5716 foreachObserver(notifyContentsCleared(DocumentObserver::Pixmap | DocumentObserver::Highlights));
5717 qCDebug(OkularCoreDebug) << "New PageSize id:" << sizeid;
5718}
5719
5720/** DocumentViewport **/
5721
5723 : pageNumber(n)
5724{
5725 // default settings
5726 rePos.enabled = false;
5727 rePos.normalizedX = 0.5;
5728 rePos.normalizedY = 0.0;
5729 rePos.pos = Center;
5730 autoFit.enabled = false;
5731 autoFit.width = false;
5732 autoFit.height = false;
5733}
5734
5736 : pageNumber(-1)
5737{
5738 // default settings (maybe overridden below)
5739 rePos.enabled = false;
5740 rePos.normalizedX = 0.5;
5741 rePos.normalizedY = 0.0;
5742 rePos.pos = Center;
5743 autoFit.enabled = false;
5744 autoFit.width = false;
5745 autoFit.height = false;
5746
5747 // check for string presence
5748 if (xmlDesc.isEmpty()) {
5749 return;
5750 }
5751
5752 // decode the string
5753 bool ok;
5754 int field = 0;
5755 QString token = xmlDesc.section(QLatin1Char(';'), field, field);
5756 while (!token.isEmpty()) {
5757 // decode the current token
5758 if (field == 0) {
5759 pageNumber = token.toInt(&ok);
5760 if (!ok) {
5761 return;
5762 }
5763 } else if (token.startsWith(QLatin1String("C1"))) {
5764 rePos.enabled = true;
5765 rePos.normalizedX = token.section(QLatin1Char(':'), 1, 1).toDouble();
5766 rePos.normalizedY = token.section(QLatin1Char(':'), 2, 2).toDouble();
5767 rePos.pos = Center;
5768 } else if (token.startsWith(QLatin1String("C2"))) {
5769 rePos.enabled = true;
5770 rePos.normalizedX = token.section(QLatin1Char(':'), 1, 1).toDouble();
5771 rePos.normalizedY = token.section(QLatin1Char(':'), 2, 2).toDouble();
5772 if (token.section(QLatin1Char(':'), 3, 3).toInt() == 1) {
5773 rePos.pos = Center;
5774 } else {
5775 rePos.pos = TopLeft;
5776 }
5777 } else if (token.startsWith(QLatin1String("AF1"))) {
5778 autoFit.enabled = true;
5779 autoFit.width = token.section(QLatin1Char(':'), 1, 1) == QLatin1String("T");
5780 autoFit.height = token.section(QLatin1Char(':'), 2, 2) == QLatin1String("T");
5781 }
5782 // proceed tokenizing string
5783 field++;
5784 token = xmlDesc.section(QLatin1Char(';'), field, field);
5785 }
5786}
5787
5789{
5790 // start string with page number
5792 // if has center coordinates, save them on string
5793 if (rePos.enabled) {
5794 s += QStringLiteral(";C2:") + QString::number(rePos.normalizedX) + QLatin1Char(':') + QString::number(rePos.normalizedY) + QLatin1Char(':') + QString::number(rePos.pos);
5795 }
5796 // if has autofit enabled, save its state on string
5797 if (autoFit.enabled) {
5798 s += QStringLiteral(";AF1:") + (autoFit.width ? QLatin1Char('T') : QLatin1Char('F')) + QLatin1Char(':') + (autoFit.height ? QLatin1Char('T') : QLatin1Char('F'));
5799 }
5800 return s;
5801}
5802
5804{
5805 return pageNumber >= 0;
5806}
5807
5809{
5810 bool equal = (pageNumber == other.pageNumber) && (rePos.enabled == other.rePos.enabled) && (autoFit.enabled == other.autoFit.enabled);
5811 if (!equal) {
5812 return false;
5813 }
5814 if (rePos.enabled && ((rePos.normalizedX != other.rePos.normalizedX) || (rePos.normalizedY != other.rePos.normalizedY) || rePos.pos != other.rePos.pos)) {
5815 return false;
5816 }
5817 if (autoFit.enabled && ((autoFit.width != other.autoFit.width) || (autoFit.height != other.autoFit.height))) {
5818 return false;
5819 }
5820 return true;
5821}
5822
5823bool DocumentViewport::operator<(const DocumentViewport &other) const
5824{
5825 // TODO: Check autoFit and Position
5826
5827 if (pageNumber != other.pageNumber) {
5828 return pageNumber < other.pageNumber;
5829 }
5830
5831 if (!rePos.enabled && other.rePos.enabled) {
5832 return true;
5833 }
5834
5835 if (!other.rePos.enabled) {
5836 return false;
5837 }
5838
5839 if (rePos.normalizedY != other.rePos.normalizedY) {
5840 return rePos.normalizedY < other.rePos.normalizedY;
5841 }
5842
5843 return rePos.normalizedX < other.rePos.normalizedX;
5844}
5845
5846/** DocumentInfo **/
5847
5849 : d(new DocumentInfoPrivate())
5850{
5851}
5852
5854 : d(new DocumentInfoPrivate())
5855{
5856 *this = info;
5857}
5858
5859DocumentInfo &DocumentInfo::operator=(const DocumentInfo &info)
5860{
5861 if (this != &info) {
5862 d->values = info.d->values;
5863 d->titles = info.d->titles;
5864 }
5865 return *this;
5866}
5867
5868DocumentInfo::~DocumentInfo()
5869{
5870 delete d;
5871}
5872
5873void DocumentInfo::set(const QString &key, const QString &value, const QString &title)
5874{
5875 d->values[key] = value;
5876 d->titles[key] = title;
5877}
5878
5879void DocumentInfo::set(Key key, const QString &value)
5880{
5881 d->values[getKeyString(key)] = value;
5882}
5883
5885{
5886 return d->values.keys();
5887}
5888
5890{
5891 return get(getKeyString(key));
5892}
5893
5895{
5896 return d->values[key];
5897}
5898
5900{
5901 switch (key) {
5902 case Title:
5903 return QStringLiteral("title");
5904 break;
5905 case Subject:
5906 return QStringLiteral("subject");
5907 break;
5908 case Description:
5909 return QStringLiteral("description");
5910 break;
5911 case Author:
5912 return QStringLiteral("author");
5913 break;
5914 case Creator:
5915 return QStringLiteral("creator");
5916 break;
5917 case Producer:
5918 return QStringLiteral("producer");
5919 break;
5920 case Copyright:
5921 return QStringLiteral("copyright");
5922 break;
5923 case Pages:
5924 return QStringLiteral("pages");
5925 break;
5926 case CreationDate:
5927 return QStringLiteral("creationDate");
5928 break;
5929 case ModificationDate:
5930 return QStringLiteral("modificationDate");
5931 break;
5932 case MimeType:
5933 return QStringLiteral("mimeType");
5934 break;
5935 case Category:
5936 return QStringLiteral("category");
5937 break;
5938 case Keywords:
5939 return QStringLiteral("keywords");
5940 break;
5941 case FilePath:
5942 return QStringLiteral("filePath");
5943 break;
5944 case DocumentSize:
5945 return QStringLiteral("documentSize");
5946 break;
5947 case PagesSize:
5948 return QStringLiteral("pageSize");
5949 break;
5950 default:
5951 qCWarning(OkularCoreDebug) << "Unknown" << key;
5952 return QString();
5953 break;
5954 }
5955}
5956
5958{
5959 if (key == QLatin1String("title")) {
5960 return Title;
5961 } else if (key == QLatin1String("subject")) {
5962 return Subject;
5963 } else if (key == QLatin1String("description")) {
5964 return Description;
5965 } else if (key == QLatin1String("author")) {
5966 return Author;
5967 } else if (key == QLatin1String("creator")) {
5968 return Creator;
5969 } else if (key == QLatin1String("producer")) {
5970 return Producer;
5971 } else if (key == QLatin1String("copyright")) {
5972 return Copyright;
5973 } else if (key == QLatin1String("pages")) {
5974 return Pages;
5975 } else if (key == QLatin1String("creationDate")) {
5976 return CreationDate;
5977 } else if (key == QLatin1String("modificationDate")) {
5978 return ModificationDate;
5979 } else if (key == QLatin1String("mimeType")) {
5980 return MimeType;
5981 } else if (key == QLatin1String("category")) {
5982 return Category;
5983 } else if (key == QLatin1String("keywords")) {
5984 return Keywords;
5985 } else if (key == QLatin1String("filePath")) {
5986 return FilePath;
5987 } else if (key == QLatin1String("documentSize")) {
5988 return DocumentSize;
5989 } else if (key == QLatin1String("pageSize")) {
5990 return PagesSize;
5991 } else {
5992 return Invalid;
5993 }
5994}
5995
5997{
5998 switch (key) {
5999 case Title:
6000 return i18n("Title");
6001 break;
6002 case Subject:
6003 return i18n("Subject");
6004 break;
6005 case Description:
6006 return i18n("Description");
6007 break;
6008 case Author:
6009 return i18n("Author");
6010 break;
6011 case Creator:
6012 return i18n("Creator");
6013 break;
6014 case Producer:
6015 return i18n("Producer");
6016 break;
6017 case Copyright:
6018 return i18n("Copyright");
6019 break;
6020 case Pages:
6021 return i18n("Pages");
6022 break;
6023 case CreationDate:
6024 return i18n("Created");
6025 break;
6026 case ModificationDate:
6027 return i18n("Modified");
6028 break;
6029 case MimeType:
6030 return i18n("MIME Type");
6031 break;
6032 case Category:
6033 return i18n("Category");
6034 break;
6035 case Keywords:
6036 return i18n("Keywords");
6037 break;
6038 case FilePath:
6039 return i18n("File Path");
6040 break;
6041 case DocumentSize:
6042 return i18n("File Size");
6043 break;
6044 case PagesSize:
6045 return i18n("Page Size");
6046 break;
6047 default:
6048 return QString();
6049 break;
6050 }
6051}
6052
6054{
6055 QString title = getKeyTitle(getKeyFromString(key));
6056 if (title.isEmpty()) {
6057 title = d->titles[key];
6058 }
6059 return title;
6060}
6061
6062/** DocumentSynopsis **/
6063
6065 : QDomDocument(QStringLiteral("DocumentSynopsis"))
6066{
6067 // void implementation, only subclassed for naming
6068}
6069
6071 : QDomDocument(document)
6072{
6073}
6074
6075/** EmbeddedFile **/
6076
6080
6084
6086 : pageNumber(page)
6087 , rect(rectangle)
6088{
6089}
6090
6091/** NewSignatureData **/
6092
6093struct Okular::NewSignatureDataPrivate {
6094 NewSignatureDataPrivate() = default;
6095
6096 QString certNickname;
6097 QString certSubjectCommonName;
6098 QString password;
6099 QString documentPassword;
6100 QString location;
6101 QString reason;
6102 QString backgroundImagePath;
6103 int page = -1;
6104 NormalizedRect boundingRectangle;
6105};
6106
6107NewSignatureData::NewSignatureData()
6108 : d(new NewSignatureDataPrivate())
6109{
6110}
6111
6112NewSignatureData::~NewSignatureData()
6113{
6114 delete d;
6115}
6116
6117QString NewSignatureData::certNickname() const
6118{
6119 return d->certNickname;
6120}
6121
6122void NewSignatureData::setCertNickname(const QString &certNickname)
6123{
6124 d->certNickname = certNickname;
6125}
6126
6127QString NewSignatureData::certSubjectCommonName() const
6128{
6129 return d->certSubjectCommonName;
6130}
6131
6132void NewSignatureData::setCertSubjectCommonName(const QString &certSubjectCommonName)
6133{
6134 d->certSubjectCommonName = certSubjectCommonName;
6135}
6136
6137QString NewSignatureData::password() const
6138{
6139 return d->password;
6140}
6141
6142void NewSignatureData::setPassword(const QString &password)
6143{
6144 d->password = password;
6145}
6146
6147int NewSignatureData::page() const
6148{
6149 return d->page;
6150}
6151
6152void NewSignatureData::setPage(int page)
6153{
6154 d->page = page;
6155}
6156
6157NormalizedRect NewSignatureData::boundingRectangle() const
6158{
6159 return d->boundingRectangle;
6160}
6161
6162void NewSignatureData::setBoundingRectangle(const NormalizedRect &rect)
6163{
6164 d->boundingRectangle = rect;
6165}
6166
6168{
6169 return d->documentPassword;
6170}
6171
6173{
6174 d->documentPassword = password;
6175}
6176
6178{
6179 return d->location;
6180}
6181
6183{
6184 d->location = location;
6185}
6186
6188{
6189 return d->reason;
6190}
6191
6193{
6194 d->reason = reason;
6195}
6196
6198{
6199 return d->backgroundImagePath;
6200}
6201
6203{
6204 d->backgroundImagePath = path;
6205}
6206
6207#undef foreachObserver
6208#undef foreachObserverD
6209
6210#include "document.moc"
6211
6212/* kate: replace-tabs on; indent-width 4; */
QStringList entries() const
const KArchiveEntry * entry(const QString &name) const
virtual bool isDirectory() const
virtual bool isFile() const
virtual bool close()
bool addLocalFile(const QString &fileName, const QString &destName)
virtual bool open(QIODevice::OpenMode mode)
bool writeFile(const QString &name, QByteArrayView data, mode_t perm=0100644, const QString &user=QString(), const QString &group=QString(), const QDateTime &atime=QDateTime(), const QDateTime &mtime=QDateTime(), const QDateTime &ctime=QDateTime())
static Q_INVOKABLE bool authorize(const QString &action)
void settingsChanged(const QString &dialogName)
QString formatByteSize(double size, int precision=1, KFormat::BinaryUnitDialect dialect=KFormat::DefaultBinaryDialect, KFormat::BinarySizeUnits units=KFormat::DefaultBinaryUnits) const
void start() override
static bool isExecutableFile(const QUrl &url, const QString &mimetypeName)
void setCurrentPage(const QModelIndex &index)
QAbstractItemModel * model() const
QString pluginId() const
QJsonObject rawData() const
QString fileName() const
static QList< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter={}, KPluginMetaDataOptions options={})
bool isValid() const
int startDetached()
QString name() const
KGroupId groupId() const
QString loginName() const
Encapsulates data that describes an action.
Definition action.h:41
QVector< Action * > nextActions() const
Returns the next actions to be executed after.
Definition action.cpp:67
@ Execute
Execute a command or external application.
Definition action.h:48
@ Goto
Goto a given page or external document.
Definition action.h:47
@ DocAction
Start a custom action.
Definition action.h:50
@ BackendOpaque
Calls back to the backend with the action.
Definition action.h:55
@ Movie
Play a movie.
Definition action.h:52
@ Browse
Browse a given website.
Definition action.h:49
@ Script
Executes a Script code.
Definition action.h:53
@ Rendition
Play a movie and/or execute a Script code.
Definition action.h:54
@ Sound
Play a sound.
Definition action.h:51
virtual ActionType actionType() const =0
Returns the type of the action.
Native annotation interface.
virtual bool supports(Capability capability) const =0
Query for the supported capabilities.
virtual void notifyRemoval(Annotation *annotation, int page)=0
Called when an existing annotation at a given page is removed.
virtual void notifyAddition(Annotation *annotation, int page)=0
Called when a new annotation is added to a page.
virtual void notifyModification(const Annotation *annotation, int page, bool appearanceChanged)=0
Called after an existing annotation at a given page is modified.
@ Modification
Generator can edit native annotations.
@ Removal
Generator can remove native annotations.
@ Addition
Generator can create native annotations.
Annotation struct holds properties shared by all annotations.
Definition annotations.h:96
QString contents() const
Returns the contents of the annotation.
int flags() const
Returns the flags of the annotation.
void setContents(const QString &contents)
Sets the contents of the annotation.
@ DenyDelete
Cannot be deleted.
@ ExternallyDrawn
Is drawn externally (by the generator which provided it)
@ BeingMoved
Is being moved (mouse drag and drop). If ExternallyDrawn, the generator must not draw it.
@ External
Is stored external.
@ DenyWrite
Cannot be changed.
@ BeingResized
Is being resized (mouse drag and drop). If ExternallyDrawn, the generator must not draw it.
QDomNode getAnnotationPropertiesDomNode() const
Retrieve the QDomNode representing this annotation's properties.
@ AHighlight
A highlight annotation.
@ AGeom
A geometrical annotation.
@ AText
A textual annotation.
@ AInk
An ink annotation.
@ ALine
A line annotation.
@ ACaret
A caret annotation.
@ AStamp
A stamp annotation.
virtual SubType subType() const =0
Returns the sub type of the annotation.
void playSound(const Sound *sound, const SoundAction *linksound=nullptr)
Enqueue the specified sound for playing, optionally taking more information about the playing from th...
void stopPlaybacks()
Tell the AudioPlayer to stop all the playbacks.
static AudioPlayer * instance()
Gets the instance of the audio player.
Bookmarks manager utility.
The Browse action browses an url by opening a web browser or email client, depending on the url proto...
Definition action.h:262
QUrl url() const
Returns the url to browse.
Definition action.cpp:251
A helper class to store information about x509 certificate.
Abstract interface for configuration control.
virtual bool reparseConfig()=0
This method is called to tell the generator to re-parse its configuration.
virtual void addPages(KConfigDialog *dialog)=0
This method allows the generator to add custom configuration pages to the config dialog of okular.
The DocumentAction action contains an action that is performed on the current document.
Definition action.h:301
DocumentActionType documentActionType() const
Returns the type of action.
Definition action.cpp:280
@ SaveAs
SaveAs the document.
Definition action.h:320
@ Quit
Quit application.
Definition action.h:313
@ Print
Print the document.
Definition action.h:319
@ PageNext
Jump to next page.
Definition action.h:309
@ Find
Open find dialog.
Definition action.h:316
@ Presentation
Start presentation.
Definition action.h:314
@ PageLast
Jump to last page.
Definition action.h:310
@ PageFirst
Jump to first page.
Definition action.h:307
@ GoToPage
Goto page.
Definition action.h:317
@ Close
Close document.
Definition action.h:318
@ HistoryForward
Go forward in page history.
Definition action.h:312
@ EndPresentation
End presentation.
Definition action.h:315
@ PagePrev
Jump to previous page.
Definition action.h:308
@ HistoryBack
Go back in page history.
Definition action.h:311
The DocumentInfo structure can be filled in by generators to display metadata about the currently ope...
Definition document.h:76
QString get(Key key) const
Returns the value for a given key or an null string when the key doesn't exist.
static Key getKeyFromString(const QString &key)
Returns the Key from a string key.
Key
The list of predefined keys.
Definition document.h:83
@ ModificationDate
The date of last modification of the document.
Definition document.h:93
@ Author
The author of the document.
Definition document.h:87
@ Subject
The subject of the document.
Definition document.h:85
@ Category
The category of the document.
Definition document.h:95
@ Producer
The producer of the document (e.g. some software)
Definition document.h:89
@ Copyright
The copyright of the document.
Definition document.h:90
@ CreationDate
The date of creation of the document.
Definition document.h:92
@ MimeType
The mime type of the document.
Definition document.h:94
@ FilePath
The path of the file.
Definition document.h:97
@ PagesSize
The size of the pages (if all pages have the same size)
Definition document.h:99
@ Keywords
The keywords which describe the content of the document.
Definition document.h:96
@ Creator
The creator of the document (this can be different from the author)
Definition document.h:88
@ Pages
The number of pages of the document.
Definition document.h:91
@ DocumentSize
The size of the document.
Definition document.h:98
@ Description
The description of the document.
Definition document.h:86
@ Invalid
An invalid key.
Definition document.h:101
@ Title
The title of the document.
Definition document.h:84
static QString getKeyString(Key key)
Returns the internal string for the given key.
void set(const QString &key, const QString &value, const QString &title=QString())
Sets a value for a custom key.
QString getKeyTitle(const QString &key) const
Returns the user visible string for the given key Takes into account keys added by the set() that tak...
DocumentInfo()
Creates a new document info.
QStringList keys() const
Returns all the keys present in this DocumentInfo.
Base class for objects being notified when something changes.
Definition observer.h:29
virtual void notifySetup(const QVector< Okular::Page * > &pages, int setupFlags)
This method is called whenever the document is initialized or reconstructed.
Definition observer.cpp:21
@ NewLayoutForPages
All the pages have.
Definition observer.h:58
@ DocumentChanged
The document is a new document.
Definition observer.h:57
@ UrlChanged
The URL has changed.
Definition observer.h:59
virtual void notifyPageChanged(int page, int flags)
This method is called whenever the content on page described by the passed flags has been changed.
Definition observer.cpp:29
virtual void notifyViewportChanged(bool smoothMove)
This method is called whenever the viewport has been changed.
Definition observer.cpp:25
@ Annotations
Annotations have been changed.
Definition observer.h:49
@ TextSelection
Text selection has been changed.
Definition observer.h:48
@ Pixmap
Pixmaps has been changed.
Definition observer.h:45
@ BoundingBox
Bounding boxes have been changed.
Definition observer.h:50
@ Highlights
Highlighting information has been changed.
Definition observer.h:47
virtual bool canUnloadPixmap(int page) const
Returns whether the observer agrees that all pixmaps for the given page can be unloaded to improve me...
Definition observer.cpp:45
A DOM tree that describes the Table of Contents.
Definition document.h:1504
DocumentSynopsis()
Creates a new document synopsis object.
A view on the document.
Definition document.h:1423
int pageNumber
The number of the page nearest the center of the viewport.
Definition document.h:1454
struct Okular::DocumentViewport::@0 rePos
If 'rePos.enabled == true' then this structure contains the viewport center or top left depending on ...
bool operator==(const DocumentViewport &other) const
bool isValid() const
Returns whether the viewport is valid.
DocumentViewport(int number=-1)
Creates a new viewport for the given page number.
struct Okular::DocumentViewport::@1 autoFit
If 'autoFit.enabled == true' then the page must be autofit in the viewport.
@ Center
Relative to the center of the page.
Definition document.h:1460
@ TopLeft
Relative to the top left corner of the page.
Definition document.h:1461
QString toString() const
Returns the viewport as xml description.
The Document.
Definition document.h:192
QStringList supportedMimeTypes() const
Returns the list with the supported MIME types.
void setAnnotationEditingEnabled(bool enable)
Control annotation editing (creation, modification and removal), which is enabled by default.
const QVector< VisiblePageRect * > & visiblePageRects() const
Returns the list of visible page rectangles.
void docdataMigrationDone()
Delete annotations and form data from the docdata folder.
void fontReadingEnded()
Reports that the reading of the fonts in the document is finished.
bool isDocdataMigrationNeeded() const
Since version 0.21, okular does not allow editing annotations and form data if they are stored in the...
const Page * page(int number) const
Returns the page object for the given page number or 0 if the number is out of range.
QByteArray requestSignedRevisionData(const Okular::SignatureInfo &info)
Returns the part of document covered by the given signature info.
BookmarkManager * bookmarkManager() const
Returns the bookmark manager of the document.
int configurableGenerators() const
Returns the number of generators that have a configuration widget.
void error(const QString &text, int duration)
This signal is emitted whenever an error occurred.
void unregisterView(View *view)
Unregister the specified view from the current document.
void linkEndPresentation()
This signal is emitted whenever an action requests an end presentation operation.
void cancelSearch()
Cancels the current search.
bool exportToText(const QString &fileName) const
Exports the document as ASCII text and saves it under fileName.
void processSourceReference(const SourceReference *reference)
Processes/Executes the given source reference.
bool canSaveChanges() const
Returns whether the changes to the document (modified annotations, values in form fields,...
PageSize::List pageSizes() const
Returns the list of supported page sizes or an empty list if this feature is not available.
void stopFontReading()
Force the termination of the reading of the information about the fonts in the document,...
void canUndoChanged(bool undoAvailable)
This signal is emitted whenever the availability of the undo function changes.
bool canSign() const
Whether the current document can perform digital signing.
void undo()
Undo last edit command.
Rotation rotation() const
Returns the current rotation of the document.
void removeObserver(DocumentObserver *observer)
Unregisters the given observer for the document.
OKULARCORE_DEPRECATED void editFormText(int pageNumber, Okular::FormFieldText *form, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
Edit the text contents of the specified form on page page to be newContents.
void setRotation(int rotation)
This slot is called whenever the user changes the rotation of the document.
void reloadDocument() const
Reloads the pixmaps for whole document.
PrintingType printingSupport() const
Returns what sort of printing the document supports: Native, Postscript, None.
bool supportsSearching() const
Returns whether the document supports searching.
static QString printErrorString(PrintError error)
QPageLayout::Orientation orientation() const
Returns the orientation of the document (for printing purposes).
@ RemoveAllPrevious
Remove all the previous requests, even for non requested page pixmaps.
Definition document.h:475
@ NoOption
No options.
Definition document.h:474
void requestTextPage(uint pageNumber)
Sends a request for text page generation for the given page pageNumber.
void continueSearch(int searchID)
Continues the search for the given searchID.
void addObserver(DocumentObserver *observer)
Registers a new observer for the document.
void fillConfigDialog(KConfigDialog *dialog)
Fill the KConfigDialog dialog with the setting pages of the generators.
bool swapBackingFile(const QString &newFileName, const QUrl &url)
Reload the document from a new location, without any visible effect to the user.
QList< ExportFormat > exportFormats() const
Returns the list of supported export formats.
bool isAllowed(Permission action) const
Returns whether the given action is allowed in the document.
void processFormMouseUpScripAction(const Action *action, Okular::FormField *ff)
Processes the mouse up action on ff.
void linkGoToPage()
This signal is emitted whenever an action requests a goto operation.
bool historyAtEnd() const
Returns whether the document history is at the end.
KXMLGUIClient * guiClient()
Returns the gui client of the generator, if it provides one.
void searchText(int searchID, const QString &text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor &color)
Searches the given text in the document.
void editFormList(int pageNumber, Okular::FormFieldChoice *form, const QList< int > &newChoices)
Edit the selected list entries in form on page page to be newChoices.
void setNextViewport()
Sets the current document viewport to the previous viewport in the viewport history.
void setPageTextSelection(int page, std::unique_ptr< RegularAreaRect > &&rect, const QColor &color)
Clears the text selection highlights for the given page, creates new ones if rect is not nullptr,...
void canRedoChanged(bool redoAvailable)
This signal is emitted whenever the availability of the redo function changes.
bool canRedo() const
Returns true if there is a redo command available; otherwise returns false.
void linkPresentation()
This signal is emitted whenever an action requests a start presentation operation.
DocumentInfo documentInfo() const
Returns the meta data of the document.
void redo()
Redo last undone edit command.
QString openError() const
Returns the reason why the file opening failed, if any.
void removePageAnnotations(int page, const QList< Annotation * > &annotations)
Removes the given annotations from the given page.
QWidget * printConfigurationWidget() const
Returns a custom printer configuration page or 0 if no custom printer configuration page is available...
void setVisiblePageRects(const QVector< VisiblePageRect * > &visiblePageRects, DocumentObserver *excludeObserver=nullptr)
Sets the list of visible page rectangles.
void setPageSize(const Okular::PageSize &size)
This slot is called whenever the user changes the page size of the document.
void processKVCFActions(Okular::FormField *ff)
A method that executes the relevant keystroke, validate, calculate and format actions on a FormField ...
DocumentAdditionalActionType
Describes the additional actions available in the Document.
Definition document.h:760
void linkFind()
This signal is emitted whenever an action requests a find operation.
bool extractArchivedFile(const QString &destFileName)
Extract the document file from the current archive.
void modifyPageAnnotationProperties(int page, Annotation *annotation)
Modifies the given annotation on the given page.
OKULARCORE_DEPRECATED void processFormatAction(const Action *action, Okular::FormFieldText *fft)
Processes the given format action on fft.
void aboutToClose()
This signal is emitted whenever the document is about to close.
CertificateStore * certificateStore() const
Returns the generator's certificate store (if any)
void gotFont(const Okular::FontInfo &font)
Emitted when a new font is found during the reading of the fonts of the document.
const DocumentViewport & viewport() const
Returns the current viewport of the document.
QList< int > bookmarkedPageList() const
Returns a list of the bookmarked.pages.
void processFocusAction(const Action *action, Okular::FormField *field)
Processes the given focus action on the field.
void requestPrint()
This signal is emitted whenever an action requests a document print operation.
void close()
This signal is emitted whenever an action requests a document close operation.
void walletDataForFile(const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey) const
Returns which wallet data to use to read/write the password for the given fileName.
QUrl currentDocument() const
Returns the url of the currently opened document.
void sourceReferenceActivated(const QString &absFileName, int line, int col, bool *handled)
This signal is emitted whenever a source reference with the given parameters has been activated.
SaveCapability
Saving capabilities.
Definition document.h:921
@ SaveAnnotationsCapability
Can save annotation changes.
Definition document.h:923
@ SaveFormsCapability
Can save form changes.
Definition document.h:922
void setViewportPage(int page, DocumentObserver *excludeObserver=nullptr, bool smoothMove=false)
Sets the current document viewport to the given page.
const QList< EmbeddedFile * > * embeddedFiles() const
Returns the list of embedded files or 0 if no embedded files are available.
void addPageAnnotation(int page, Annotation *annotation)
Adds a new annotation to the given page.
OpenResult openDocumentArchive(const QString &docFile, const QUrl &url, const QString &password=QString())
Opens a document archive.
KPluginMetaData generatorInfo() const
Returns the metadata associated with the generator.
QString bookmarkedPageRange() const
Returns the range of the bookmarked.pages.
void requestPixmaps(const QList< PixmapRequest * > &requests)
Sends requests for pixmap generation.
void registerView(View *view)
Register the specified view for the current document.
bool historyAtBegin() const
Returns whether the document history is at the begin.
QByteArray fontData(const FontInfo &font) const
Gets the font data for the given font.
void setNextDocumentDestination(const QString &namedDestination)
Sets the next namedDestination in the viewport history.
void resetSearch(int searchID)
Resets the search for the given searchID.
bool canSwapBackingFile() const
Returns whether the generator supports hot-swapping the current file with another identical file.
void setZoom(int factor, DocumentObserver *excludeObserver=nullptr)
Sets the zoom for the current document.
void requestSaveAs()
This signal is emitted whenever an action requests a document save as operation.
void quit()
This signal is emitted whenever an action requests an application quit operation.
void fontReadingProgress(int page)
Reports the progress when reading the fonts in the document.
OpenResult openDocument(const QString &docFile, const QUrl &url, const QMimeType &mime, const QString &password=QString())
Opens the document.
void editFormButtons(int pageNumber, const QList< Okular::FormFieldButton * > &formButtons, const QList< bool > &newButtonStates)
Set the states of the group of form buttons formButtons on page page to newButtonStates.
bool saveDocumentArchive(const QString &fileName)
Saves a document archive.
void reparseConfig()
Reparses and applies the configuration.
OKULARCORE_DEPRECATED void processValidateAction(const Action *action, Okular::FormFieldText *fft, bool &returnCode)
Processes the given keystroke action on fft.
bool exportTo(const QString &fileName, const ExportFormat &format) const
Exports the document in the given format and saves it under fileName.
@ NoMatchFound
No match was found.
Definition document.h:625
void setViewport(const DocumentViewport &viewport, DocumentObserver *excludeObserver=nullptr, bool smoothMove=false, bool updateHistory=true)
Sets the current document viewport to the given viewport.
void processAction(const Action *action)
Processes the given action.
~Document() override
Destroys the document.
Document::PrintError print(QPrinter &printer)
Prints the document to the given printer.
void translatePageAnnotation(int page, Annotation *annotation, const Okular::NormalizedPoint &delta)
Translates the position of the given annotation on the given page by a distance delta in normalized c...
bool supportsPageSizes() const
Returns whether the document supports the listing of page sizes.
bool sign(const NewSignatureData &data, const QString &newPath)
Digitally sign document.
QVariant metaData(const QString &key, const QVariant &option=QVariant()) const
Returns the meta data for the given key and option or an empty variant if the key doesn't exists.
OKULARCORE_DEPRECATED void processKeystrokeAction(const Action *action, Okular::FormFieldText *fft, const QVariant &newValue)
Processes the given keystroke action on fft.
const SourceReference * dynamicSourceReference(int pageNr, double absX, double absY)
Asks the generator to dynamically generate a SourceReference for a given page number and absolute X a...
void setEditorCommandOverride(const QString &editCmd)
sets the editor command to the command editCmd, as given at the commandline.
void setHistoryClean(bool clean)
Sets the history to be clean.
void searchFinished(int searchID, Okular::Document::SearchStatus endStatus)
Reports that the current search finished.
@ NoPrintError
Printing succeeded.
Definition document.h:817
const DocumentSynopsis * documentSynopsis() const
Returns the table of content of the document or 0 if no table of content is available.
void refreshFormWidget(Okular::FormField *field)
This signal is emitted whenever a FormField was changed programmatically and the according widget sho...
bool canModifyPageAnnotation(const Annotation *annotation) const
Tests if the annotation can be modified.
QSizeF allPagesSize() const
If all pages have the same size this method returns it, if the page sizes differ an empty size object...
void closeDocument()
Closes the document.
bool canUndo() const
Returns true if there is an undo command available; otherwise returns false.
bool saveChanges(const QString &fileName)
Save the document and the optional changes to it to the specified fileName.
QString pageSizeString(int page) const
Returns the size string for the given page or an empty string if the page is out of range.
bool supportsTiles() const
Returns whether the current document supports tiles.
OKULARCORE_DEPRECATED void processKeystrokeCommitAction(const Action *action, Okular::FormFieldText *fft)
Processes the given keystroke action on fft.
bool isOpened() const
Returns whether the document is currently opened.
void setPrevViewport()
Sets the current document viewport to the next viewport in the viewport history.
bool canConfigurePrinter() const
Returns whether the document can configure the printer itself.
void undoHistoryCleanChanged(bool clean)
This signal is emitted whenever the undo history is clean (i.e.
QString editorCommandOverride() const
returns the overriding editor command.
PrintingType
What type of printing a document supports.
Definition document.h:798
@ PostscriptPrinting
Postscript file printing.
Definition document.h:801
@ NativePrinting
Native Cross-Platform Printing.
Definition document.h:800
@ NoPrinting
Printing Not Supported.
Definition document.h:799
void setNextDocumentViewport(const DocumentViewport &viewport)
Sets the next viewport in the viewport history.
bool canExportToText() const
Returns whether the document supports the export to ASCII text.
void processMovieAction(const Okular::MovieAction *action)
This signal is emitted whenever an movie action is triggered and the UI should process it.
uint currentPage() const
Returns the number of the current page.
void processRenditionAction(const Okular::RenditionAction *action)
This signal is emitted whenever an rendition action is triggered and the UI should process it.
OpenResult
Describes the result of an open document operation.
Definition document.h:210
void editPageAnnotationContents(int page, Annotation *annotation, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
Edits the plain text contents of the given annotation on the given page.
void refreshPixmaps(int pageNumber)
Refresh the pixmaps for the given pageNumber.
bool swapBackingFileArchive(const QString &newFileName, const QUrl &url)
Same as swapBackingFile, but newFileName must be a .okular file.
QAbstractItemModel * layersModel() const
Returns the model for rendering layers (NULL if the document has no layers)
bool canRemovePageAnnotation(const Annotation *annotation) const
Tests if the annotation can be removed.
uint pages() const
Returns the number of pages of the document.
void startFontReading()
Starts the reading of the information about the fonts in the document, if available.
bool canProvideFontInformation() const
Whether the current document can provide information about the fonts used in it.
SearchType
Describes the possible search types.
Definition document.h:612
@ PreviousMatch
Search previous match.
Definition document.h:614
@ GoogleAny
Search complete document (any words in google style)
Definition document.h:617
@ AllDocument
Search complete document.
Definition document.h:615
@ GoogleAll
Search complete document (all words in google style)
Definition document.h:616
@ NextMatch
Search next match.
Definition document.h:613
void editFormCombo(int pageNumber, Okular::FormFieldChoice *form, const QString &newText, int newCursorPos, int prevCursorPos, int prevAnchorPos)
Set the active choice in the combo box form on page page to newText The new cursor position (newCurso...
bool supportsPrintToFile() const
Returns whether the document supports printing to both PDF and PS files.
void adjustPageAnnotation(int page, Annotation *annotation, const Okular::NormalizedPoint &delta1, const Okular::NormalizedPoint &delta2)
Adjusts the position of the top-left and bottom-right corners of given annotation on the given page.
void removePageAnnotation(int page, Annotation *annotation)
Removes the given annotation from the given page.
void processDocumentAction(const Action *action, DocumentAdditionalActionType type)
Processes the given document additional action of specified type.
void prepareToModifyAnnotationProperties(Annotation *annotation)
Prepares to modify the properties of the given annotation.
EmbeddedFile()
Creates a new embedded file.
virtual ~EmbeddedFile()
Destroys the embedded file.
The Execute action executes an external application.
Definition action.h:217
QString fileName() const
Returns the file name of the application to execute.
Definition action.cpp:200
QString parameters() const
Returns the parameters of the application to execute.
Definition action.cpp:206
Defines an entry for the export menu.
Definition generator.h:79
A small class that represents the information of a font.
Definition fontinfo.h:25
Interface of a choice form field.
Definition form.h:421
virtual QString editChoice() const
The text entered into an editable combo box choice field.
Definition form.cpp:350
virtual QList< int > currentChoices() const =0
The currently selected choices.
virtual QStringList choices() const =0
The possible choices of the choice field.
Interface of a text form field.
Definition form.h:310
virtual void setAppearanceText(const QString &text)=0
Set the text which should be rendered by the PDF.
virtual void setText(const QString &text)
Sets the new text in the text field.
Definition form.cpp:241
virtual QString text() const =0
The text of text field.
The base interface of a form field.
Definition form.h:40
virtual bool isReadOnly() const
Whether the field is read-only.
Definition form.cpp:53
Action * additionalAction(AdditionalActionType type) const
Returns the additional action of the given type or nullptr if no action has been defined.
Definition form.cpp:106
@ FieldModified
An action to be performed when the user modifies the field.
Definition form.h:161
@ CalculateField
An action to be performed when the field needs to be recalculated.
Definition form.h:164
@ ValidateField
An action to be performed when the field value changes.
Definition form.h:163
@ FormatField
An action to be performed before the field is formatted to display its value.
Definition form.h:162
void commitFormattedValue(const QString &value)
Updates the value that was last committed in this form field.
Definition form.cpp:163
virtual void setValue(const QVariant &value)
Sets the value associated with the form field.
Definition form.cpp:80
virtual void setAppearanceValue(const QVariant &value)
Sets the appearance value associated with the form field.
Definition form.cpp:84
QString committedValue() const
Returns the value that was last committed in this form field.
Definition form.cpp:145
void commitValue(const QString &value)
Updates the value that was last committed in this form field.
Definition form.cpp:151
QString committedFormattedValue() const
Returns the formatted value that was last committed in this form field.
Definition form.cpp:157
virtual QVariant value() const
Returns the value associated wit the form field.
Definition form.cpp:88
[Abstract Class] The information generator.
Definition generator.h:188
void error(const QString &message, int duration)
This signal should be emitted whenever an error occurred in the generator.
SwapBackingFileResult
Describes the result of an swap file operation.
Definition generator.h:278
DocumentMetaDataKey
Internal document setting.
Definition generator.h:549
@ TextHintingMetaData
Returns (bool)text hinting from Settings (option is not used)
Definition generator.h:553
@ GraphicsAntialiasMetaData
Returns (bool)graphic antialias from Settings (option is not used)
Definition generator.h:552
@ TextAntialiasMetaData
Returns (bool) text antialias from Settings (option is not used)
Definition generator.h:551
@ PaperColorMetaData
Returns (QColor) the paper color if set in Settings or the default color (white) if option is true (o...
Definition generator.h:550
void notice(const QString &message, int duration)
This signal should be emitted whenever the user should be noticed.
@ Threaded
Whether the Generator supports asynchronous generation of pictures or text pages.
Definition generator.h:202
@ PrintToFile
Whether the Generator supports export to PDF & PS through the Print Dialog.
Definition generator.h:209
@ ReadRawData
Whether the Generator can read a document directly from its raw data.
Definition generator.h:204
@ TextExtraction
Whether the Generator can extract text from the document in the form of TextPage's.
Definition generator.h:203
@ TiledRendering
Whether the Generator can render tiles.
Definition generator.h:210
@ SwapBackingFile
Whether the Generator can hot-swap the file it's reading from.
Definition generator.h:211
@ PrintNative
Whether the Generator supports native cross-platform printing (QPainter-based).
Definition generator.h:207
@ FontInfo
Whether the Generator can provide information about the fonts used in the document.
Definition generator.h:205
@ PageSizes
Whether the Generator can change the size of the document pages.
Definition generator.h:206
@ SupportsCancelling
Whether the Generator can cancel requests.
Definition generator.h:212
@ PrintPostscript
Whether the Generator supports postscript-based file printing.
Definition generator.h:208
@ Points
The page size is given in 1/72 inches.
Definition generator.h:369
@ Pixels
The page size is given in screen pixels.
Definition generator.h:370
@ None
The page size is not defined in a physical metric.
Definition generator.h:368
void warning(const QString &message, int duration)
This signal should be emitted whenever the user should be warned.
The Goto action changes the viewport to another page or loads an external document.
Definition action.h:151
bool isExternal() const
Returns whether the goto action points to an external document.
Definition action.cpp:140
QString destinationName() const
Returns the document named destination the goto action points to.
Definition action.cpp:158
QString fileName() const
Returns the filename of the external document.
Definition action.cpp:146
DocumentViewport destViewport() const
Returns the document viewport the goto action points to.
Definition action.cpp:152
Abstract interface for user interface control.
KXMLGUIClient * guiClient()
This method requests the XML GUI Client provided by the interface.
The Movie action executes an operation on a video on activation.
Definition action.h:469
Data needed to create a new signature.
Definition document.h:1611
QString documentPassword() const
void setReason(const QString &reason)
void setDocumentPassword(const QString &password)
void setLocation(const QString &location)
QString reason() const
QString backgroundImagePath() const
void setBackgroundImagePath(const QString &path)
QString location() const
NormalizedPoint is a helper class which stores the coordinates of a normalized point.
Definition area.h:117
A NormalizedRect is a rectangle which can be defined by two NormalizedPoints.
Definition area.h:189
double bottom
The normalized bottom coordinate.
Definition area.h:435
double right
The normalized right coordinate.
Definition area.h:430
double height() const
Definition area.h:412
double left
The normalized left coordinate.
Definition area.h:420
double width() const
Definition area.h:406
QRect geometry(int xScale, int yScale) const
Returns the rectangle mapped to a reference area of xScale x yScale.
Definition area.cpp:232
double top
The normalized top coordinate.
Definition area.h:425
bool isNull() const
Returns whether this normalized rectangle is a null normalized rect.
Definition area.cpp:155
An area with normalized coordinates that contains a reference to an object.
Definition area.h:458
@ Action
An action.
Definition area.h:464
A small class that represents the size of a page.
Definition pagesize.h:24
Collector for all the data belonging to a page.
Definition page.h:48
void deletePixmap(DocumentObserver *observer)
Deletes the pixmap for the given observer.
Definition page.cpp:753
double height() const
Returns the height of the page.
Definition page.cpp:185
bool hasPixmap(DocumentObserver *observer, int width=-1, int height=-1, const NormalizedRect &rect=NormalizedRect()) const
Returns whether the page of size width x height has a pixmap in the region given by rect for the give...
Definition page.cpp:219
double width() const
Returns the width of the page.
Definition page.cpp:180
void setPageSize(DocumentObserver *observer, int width, int height)
Sets the size of the page (in screen pixels) if there is a TilesManager.
Definition page.cpp:248
Rotation rotation() const
Returns the rotation of the page as defined by the user.
Definition page.cpp:170
Describes a pixmap type request.
Definition generator.h:618
bool preload() const
Returns whether the generation request is for a page that is not important i.e.
void setTile(bool tile)
Sets whether the generator should render only the given normalized rect or the entire page.
bool asynchronous() const
Returns whether the generation should be done synchronous or asynchronous.
bool shouldAbortRender() const
Should the request be aborted if possible?
void setPartialUpdatesWanted(bool partialUpdatesWanted)
Sets whether the request should report back updates if possible.
int width() const
Returns the page width of the requested pixmap.
int height() const
Returns the page height of the requested pixmap.
int priority() const
Returns the priority (less it better, 0 is maximum) of the request.
const NormalizedRect & normalizedRect() const
Returns the normalized region of the page to request.
bool partialUpdatesWanted() const
Should the request report back updates if possible?
int pageNumber() const
Returns the page number of the request.
void setNormalizedRect(const NormalizedRect &rect)
Sets the region of the page to request.
Page * page() const
Returns a pointer to the page where the pixmap shall be generated for.
bool isTile() const
Returns whether the generator should render just the region given by normalizedRect() or the entire p...
DocumentObserver * observer() const
Returns the observer of the request.
Abstract interface for advanced printing control.
virtual QWidget * printConfigurationWidget() const =0
Builds and returns a new printing configuration widget.
This is a list of NormalizedRect, to describe an area consisting of multiple rectangles using normali...
Definition area.h:933
The Rendition action executes an operation on a video or executes some JavaScript code on activation.
Definition action.h:523
ScriptType scriptType() const
Returns the type of script.
Definition action.cpp:578
QString script() const
Returns the script code.
Definition action.cpp:584
Abstract interface for saving.
virtual AnnotationProxy * annotationProxy() const =0
Returns the annotation proxy.
virtual bool save(const QString &fileName, SaveOptions options, QString *errorText)=0
Save to the specified fileName with the specified options.
virtual bool supportsOption(SaveOption option) const =0
Query for the supported saving options.
@ SaveChanges
The possibility to save with the current changes to the document.
The Script action executes a Script code.
Definition action.h:423
ScriptType scriptType() const
Returns the type of action.
Definition action.cpp:441
QString script() const
Returns the code.
Definition action.cpp:447
The Sound action plays a sound on activation.
Definition action.h:359
Okular::Sound * sound() const
Returns the sound object which contains the sound data.
Definition action.cpp:394
This class describes the object rectangle for a source reference.
Definition area.h:599
Defines a source reference.
int column() const
Returns the column of the position in the source file.
int row() const
Returns the row of the position in the source file.
QString fileName() const
Returns the filename of the source.
This class represents a rectangular portion of a page.
Definition tile.h:23
NormalizedRect rect() const
Location of the tile.
bool isValid() const
True if the pixmap is available and updated.
static QSizeF realDpi(const QWindow *windowOnScreen)
Return the real DPI of the display containing given window.
Definition utils.cpp:47
View on the document.
Definition view.h:30
Document * viewDocument() const
Return the document which this view is associated to, or null if it is not associated with any docume...
Definition view.cpp:39
VisiblePageRect(int pageNumber=-1, const NormalizedRect &rectangle=NormalizedRect())
Creates a new visible page rectangle.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
char * toString(const EngineQuery &query)
KCALUTILS_EXPORT QString mimeType()
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QString name(GameStandardAction id)
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
KIOCORE_EXPORT StatJob * stat(const QUrl &url, JobFlags flags=DefaultFlags)
KIOCORE_EXPORT QUrl upUrl(const QUrl &url)
KIOCORE_EXPORT KJobUiDelegate * createDefaultJobUiDelegate()
QWidget * window(QObject *job)
QString expandMacrosShellQuote(const QString &str, const QHash< QChar, QString > &map, QChar c=QLatin1Char('%'))
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
KCOREADDONS_EXPORT QStringList splitArgs(const QString &cmd, Options flags=NoOptions, Errors *err=nullptr)
QAction * zoom(const QObject *recvr, const char *slot, QObject *parent)
KGuiItem forward(BidiMode useBidi=IgnoreRTL)
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
global.h
Definition action.h:17
@ JavaScript
JavaScript code.
Definition global.h:76
Permission
Describes the DRM capabilities.
Definition global.h:24
@ AllowFillForms
Allows to fill the forms in the document.
Definition global.h:29
@ AllowNotes
Allows to add annotations to the document.
Definition global.h:28
Rotation
A rotation.
Definition global.h:46
@ Rotation270
Rotated 2700 degrees clockwise.
Definition global.h:50
@ Rotation90
Rotated 90 degrees clockwise.
Definition global.h:48
@ Rotation0
Not rotated.
Definition global.h:47
@ FromTop
Searching from top of the page, next result is to be found, there was no earlier search result.
Definition global.h:37
@ FromBottom
Searching from bottom of the page, next result is to be found, there was no earlier search result.
Definition global.h:38
@ PreviousResult
Searching for the previous result on the page, earlier result should be located so we search from the...
Definition global.h:40
@ NextResult
Searching for the next result on the page, earlier result should be located so we search from the las...
Definition global.h:39
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
const char * constData() const const
bool isEmpty() const const
qsizetype size() const const
QChar fromLatin1(char c)
bool isHighSurrogate(char32_t ucs4)
QColor fromHsv(int h, int s, int v, int a)
bool openUrl(const QUrl &url)
bool isRelativePath(const QString &path)
bool mkpath(const QString &dirPath) const const
QString tempPath()
QDomElement createElement(const QString &tagName)
QDomProcessingInstruction createProcessingInstruction(const QString &target, const QString &data)
QDomText createTextNode(const QString &value)
QDomElement documentElement() const const
ParseResult setContent(QAnyStringView text, ParseOptions options)
QByteArray toByteArray(int indent) const const
QString attribute(const QString &name, const QString &defValue) const const
bool hasAttribute(const QString &name) const const
void setAttribute(const QString &name, const QString &value)
QString tagName() const const
QString text() const const
QDomNode appendChild(const QDomNode &newChild)
void clear()
QDomNode firstChild() const const
bool isElement() const const
bool isNull() const const
QDomNode nextSibling() const const
QDomDocument ownerDocument() const const
QDomElement toElement() const const
int exec(ProcessEventsFlags flags)
QString decodeName(const QByteArray &localFileName)
QByteArray encodeName(const QString &fileName)
bool exists(const QString &fileName)
bool exists() const const
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
bool remove()
virtual void close() override
virtual bool seek(qint64 pos) override
bool exists() const const
QString symLinkTarget() const const
void restoreOverrideCursor()
void setOverrideCursor(const QCursor &cursor)
const_iterator constBegin() const const
const_iterator constEnd() const const
const_iterator constFind(const Key &key) const const
iterator end()
iterator find(const Key &key)
bool isEmpty() const const
T value(const Key &key) const const
QByteArray read(qint64 maxSize)
QByteArray readAll()
QJsonValue value(QLatin1StringView key) const const
int toInt(int defaultValue) const const
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
iterator begin()
void clear()
const_iterator constBegin() const const
const_iterator constEnd() const const
bool contains(const AT &value) const const
qsizetype count() const const
iterator end()
T & first()
bool isEmpty() const const
T & last()
void push_back(parameter_type value)
qsizetype size() const const
value_type takeFirst()
QLocale system()
iterator begin()
const_iterator constBegin() const const
const_iterator constEnd() const const
bool contains(const Key &key) const const
size_type count() const const
iterator end()
iterator insert(const Key &key, const T &value)
Key key(const T &value, const Key &defaultKey) const const
size_type remove(const Key &key)
QMimeType mimeTypeForData(QIODevice *device) const const
QMimeType mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode) const const
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
QMimeType mimeTypeForUrl(const QUrl &url) const const
bool inherits(const QString &mimeTypeName) const const
bool isValid() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
virtual bool event(QEvent *e)
T qobject_cast(QObject *object)
PageSizeId id() const const
QString name() const const
int height() const const
int width() const const
int height() const const
int width() const const
void clear()
bool contains(const QSet< T > &other) const const
iterator insert(const T &value)
bool isEmpty() const const
T * get() const const
int height() const const
int width() const const
qreal height() const const
bool isValid() const const
qreal width() const const
void push(const T &t)
T & top()
QString findExecutable(const QString &executableName, const QStringList &paths)
QString writableLocation(StandardLocation type)
QString & append(QChar ch)
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromUcs4(const char32_t *unicode, qsizetype size)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
QString & insert(qsizetype position, QChar ch)
bool isEmpty() const const
bool isNull() const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString section(QChar sep, qsizetype start, qsizetype end, SectionFlags flags) const const
qsizetype size() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
double toDouble(bool *ok) const const
int toInt(bool *ok, int base) const const
std::u32string toStdU32String() const const
qulonglong toULongLong(bool *ok, int base) const const
QString trimmed() const const
CaseSensitivity
WaitCursor
SkipEmptyParts
virtual QString fileName() const const override
void timeout()
QTransform inverted(bool *invertible) const const
void canRedoChanged(bool canRedo)
void canUndoChanged(bool canUndo)
void cleanChanged(bool clean)
PreferLocalFile
QString fileName(ComponentFormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
QString host(ComponentFormattingOptions options) const const
bool isEmpty() const const
bool isLocalFile() const const
bool isRelative() const const
bool isValid() const const
QString path(ComponentFormattingOptions options) const const
QUrl resolved(const QUrl &relative) const const
QString scheme() const const
QString toLocalFile() const const
QString url(FormattingOptions options) const const
bool toBool() const const
QString toString() const const
T value() const const
QScreen * screen() const const
QWidget * window() const const
QWindow * windowHandle() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:51:37 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.