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