Okular

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