Okular

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