Okular

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