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