Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_safe_document_loader.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2013 Dmitry Kazakov <dimula73@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
9#include <QTimer>
10#include <QFileSystemWatcher>
11#include <QRandomGenerator>
12#include <QApplication>
13#include <QFileInfo>
14#include <QDir>
15#include <QUrl>
16
17#include <KoStore.h>
18
19#include <kis_paint_layer.h>
20#include <kis_group_layer.h>
21#include "KisDocument.h"
22#include <kis_image.h>
24#include "KisPart.h"
25#include "KisUsageLogger.h"
26
27#include <kis_layer_utils.h>
28#include <kis_global.h>
29
30class FileSystemWatcherWrapper : public QObject
31{
32 Q_OBJECT
33
34private:
40
41 struct FileEntry
42 {
44 QElapsedTimer lostTimer;
46 };
47
48public:
50 : m_reattachmentCompressor(100, KisSignalCompressor::FIRST_INACTIVE),
51 m_lostCompressor(1000, KisSignalCompressor::FIRST_INACTIVE)
52
53 {
54 connect(&m_watcher, SIGNAL(fileChanged(QString)), SLOT(slotFileChanged(QString)));
55 connect(&m_reattachmentCompressor, SIGNAL(timeout()), SLOT(slotReattachFiles()));
56 connect(&m_lostCompressor, SIGNAL(timeout()), SLOT(slotFindLostFiles()));
57 }
58
59 bool addPath(const QString &file) {
60 bool result = true;
61 const QString ufile = unifyFilePath(file);
62
63 if (m_fileEntries.contains(ufile)) {
64 m_fileEntries[ufile].numConnections++;
65 } else {
66 m_fileEntries.insert(ufile, {1, {}, Exists});
67 result = m_watcher.addPath(ufile);
68 }
69
70 return result;
71 }
72
73 bool removePath(const QString &file) {
74 bool result = true;
75 const QString ufile = unifyFilePath(file);
76
78
79 if (m_fileEntries[ufile].numConnections == 1) {
80 m_fileEntries.remove(ufile);
81 result = m_watcher.removePath(ufile);
82 } else {
83 m_fileEntries[ufile].numConnections--;
84 }
85 return result;
86 }
87
89 return m_watcher.files();
90 }
91
92private Q_SLOTS:
93 void slotFileChanged(const QString &path) {
94
96
97 FileEntry &entry = m_fileEntries[path];
98
99 // re-add the file after QSaveFile optimization
100 if (!m_watcher.files().contains(path)) {
101
102 if (QFileInfo(path).exists()) {
103 const FileState oldState = entry.state;
104
105 m_watcher.addPath(path);
106 entry.state = Exists;
107
108 if (oldState == Lost) {
109 Q_EMIT fileExistsStateChanged(path, true);
110 } else {
111 Q_EMIT fileChanged(path);
112 }
113
114 } else {
115
116 if (entry.state == Exists) {
117 entry.state = Reattaching;
118 entry.lostTimer.start();
120
121 } else if (entry.state == Reattaching) {
122 if (entry.lostTimer.elapsed() > 10000) {
123 entry.state = Lost;
125 Q_EMIT fileExistsStateChanged(path, false);
126 } else {
128 }
129
130 } else if (entry.state == Lost) {
132 }
133
134
135#if 0
136 const bool shouldSpitWarning =
137 absenceTimeMSec <= 600000 &&
138 ((absenceTimeMSec >= 60000 && (absenceTimeMSec % 60000 == 0)) ||
139 (absenceTimeMSec >= 10000 && (absenceTimeMSec % 10000 == 0)));
140
141 if (shouldSpitWarning) {
142 QString message;
143 QTextStream log(&message);
145
146 log << "WARNING: couldn't reconnect to a removed file layer's file (" << path << "). File is not available for " << absenceTimeMSec / 1000 << " seconds";
147
148 qWarning() << message;
149 KisUsageLogger::log(message);
150
151 if (absenceTimeMSec == 600000) {
152 message.clear();
153 log.reset();
154
155 log << "Giving up... :( No more reports about " << path;
156
157 qWarning() << message;
158 KisUsageLogger::log(message);
159 }
160 }
161#endif
162 }
163 } else {
164 Q_EMIT fileChanged(path);
165 }
166 }
167
169 for (auto it = m_fileEntries.constBegin(); it != m_fileEntries.constEnd(); ++it) {
170 if (it.value().state == Lost)
171 slotFileChanged(it.key());
172 }
173 }
174
176 for (auto it = m_fileEntries.constBegin(); it != m_fileEntries.constEnd(); ++it) {
177 if (it.value().state == Reattaching)
178 slotFileChanged(it.key());
179 }
180 }
181
182
183Q_SIGNALS:
184 void fileChanged(const QString &path);
185 void fileExistsStateChanged(const QString &path, bool exists);
186
187public:
188 static QString unifyFilePath(const QString &path) {
189 return QFileInfo(path).absoluteFilePath();
190 }
191
192private:
193 QFileSystemWatcher m_watcher;
194 QHash<QString, int> m_pathCount;
197 QHash<QString, int> m_lostFilesAbsenceCounter;
198 QHash<QString, FileEntry> m_fileEntries;
199};
200
201Q_GLOBAL_STATIC(FileSystemWatcherWrapper, s_fileSystemWatcher)
202
203
205{
207 : fileChangedSignalCompressor(500 /* ms */, KisSignalCompressor::POSTPONE)
208 {
209 }
210
211 QScopedPointer<KisDocument> doc;
213 bool isLoading = false;
214 bool fileChangedFlag = false;
215 QString path;
217
218 qint64 initialFileSize {0};
220
221 int failureCount {0};
222};
223
224KisSafeDocumentLoader::KisSafeDocumentLoader(const QString &path, QObject *parent)
225 : QObject(parent),
226 m_d(new Private())
227{
228 connect(s_fileSystemWatcher, SIGNAL(fileChanged(QString)),
229 SLOT(fileChanged(QString)));
230
231 connect(s_fileSystemWatcher, SIGNAL(fileExistsStateChanged(QString, bool)),
232 SLOT(slotFileExistsStateChanged(QString, bool)));
233
234 connect(&m_d->fileChangedSignalCompressor, SIGNAL(timeout()),
235 SLOT(fileChangedCompressed()));
236
237 setPath(path);
238}
239
241{
242 if (!m_d->path.isEmpty()) {
243 s_fileSystemWatcher->removePath(m_d->path);
244 }
245
246 delete m_d;
247}
248
249void KisSafeDocumentLoader::setPath(const QString &path)
250{
251 if (path.isEmpty()) return;
252
253 if (!m_d->path.isEmpty()) {
254 s_fileSystemWatcher->removePath(m_d->path);
255 }
256
257 m_d->path = path;
258 s_fileSystemWatcher->addPath(m_d->path);
259}
260
265
273
274void KisSafeDocumentLoader::slotFileExistsStateChanged(const QString &path, bool fileExists)
275{
277 Q_EMIT fileExistsStateChanged(fileExists);
278 if (fileExists) {
279 fileChanged(path);
280 }
281 }
282}
283
285{
286 if (m_d->isLoading) return;
287
288 QFileInfo initialFileInfo(m_d->path);
289 m_d->initialFileSize = initialFileInfo.size();
290 m_d->initialFileTimeStamp = initialFileInfo.lastModified();
291
292 // it may happen when the file is flushed by
293 // so other application
294 if (!m_d->initialFileSize) return;
295
296 m_d->isLoading = true;
297 m_d->fileChangedFlag = false;
298
300 QDir::tempPath() + '/' +
301 QString("krita_file_layer_copy_%1_%2.%3")
302 .arg(QApplication::applicationPid())
303 .arg(QRandomGenerator::global()->generate())
304 .arg(initialFileInfo.suffix());
305
306 QFile::copy(m_d->path, m_d->temporaryPath);
307
308
309 if (!sync) {
310 QTimer::singleShot(100, Qt::CoarseTimer, this, SLOT(delayedLoadStart()));
311 } else {
312 QApplication::processEvents();
314 }
315}
316
318{
319 QFileInfo originalInfo(m_d->path);
320 QFileInfo tempInfo(m_d->temporaryPath);
321 bool successfullyLoaded = false;
322
323 if (!m_d->fileChangedFlag &&
324 originalInfo.size() == m_d->initialFileSize &&
325 originalInfo.lastModified() == m_d->initialFileTimeStamp &&
326 tempInfo.size() == m_d->initialFileSize) {
327
328 m_d->doc.reset(KisPart::instance()->createTemporaryDocument());
329 m_d->doc->setFileBatchMode(true);
330
331 if (m_d->path.toLower().endsWith("ora") || m_d->path.toLower().endsWith("kra")) {
332 QScopedPointer<KoStore> store(KoStore::createStore(m_d->temporaryPath, KoStore::Read));
333 if (store && !store->bad()) {
334 if (store->open(QString("mergedimage.png"))) {
335 QByteArray bytes = store->read(store->size());
336 store->close();
337 QImage mergedImage;
338 mergedImage.loadFromData(bytes);
339 Q_ASSERT(!mergedImage.isNull());
340 KisImageSP image = new KisImage(0, mergedImage.width(), mergedImage.height(), KoColorSpaceRegistry::instance()->rgb8(), "");
341
342 constexpr double DOTS_PER_METER_TO_DOTS_PER_INCH = 0.00035285815102328864;
343 double xres = mergedImage.dotsPerMeterX() * DOTS_PER_METER_TO_DOTS_PER_INCH;
344 double yres = mergedImage.dotsPerMeterY() * DOTS_PER_METER_TO_DOTS_PER_INCH;
345
346 image->setResolution(xres, yres);
347 KisPaintLayerSP layer = new KisPaintLayer(image, "", OPACITY_OPAQUE_U8);
348 layer->paintDevice()->convertFromQImage(mergedImage, 0);
349 image->addNode(layer, image->rootLayer());
350 image->initialRefreshGraph();
351 m_d->doc->setCurrentImage(image);
352 successfullyLoaded = true;
353 }
354 else {
355 qWarning() << "delayedLoadStart: Could not open mergedimage.png";
356 }
357 }
358 else {
359 qWarning() << "delayedLoadStart: Store was bad";
360 }
361 }
362 else {
363 successfullyLoaded = m_d->doc->openPath(m_d->temporaryPath,
365
366 if (successfullyLoaded) {
367 // Wait for required updates, if any. BUG: 448256
369 m_d->doc->image()->waitForDone();
370 }
371 }
372 } else {
373 dbgKrita << "File was modified externally. Restarting.";
377 dbgKrita << ppVar(originalInfo.size());
378 dbgKrita << ppVar(originalInfo.lastModified());
379 dbgKrita << ppVar(tempInfo.size());
380 }
381
382 QFile::remove(m_d->temporaryPath);
383 m_d->isLoading = false;
384
385 if (!successfullyLoaded) {
386 // Restart the attempt
387 m_d->failureCount++;
388 if (m_d->failureCount >= 3) {
389 Q_EMIT loadingFailed();
390 }
391 else {
393 }
394 }
395 else {
396 KisPaintDeviceSP paintDevice = new KisPaintDevice(m_d->doc->image()->colorSpace());
397 KisPaintDeviceSP projection = m_d->doc->image()->projection();
398 paintDevice->makeCloneFrom(projection, projection->extent());
399 Q_EMIT loadingFinished(paintDevice,
400 m_d->doc->image()->xRes(),
401 m_d->doc->image()->yRes(),
402 m_d->doc->image()->size());
403 }
404
405 m_d->doc.reset();
406}
407
408#include "kis_safe_document_loader.moc"
Q_GLOBAL_STATIC(KisStoragePluginRegistry, s_instance)
const quint8 OPACITY_OPAQUE_U8
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
static QString unifyFilePath(const QString &path)
bool addPath(const QString &file)
QHash< QString, FileEntry > m_fileEntries
KisSignalCompressor m_reattachmentCompressor
void slotFileChanged(const QString &path)
void fileExistsStateChanged(const QString &path, bool exists)
bool removePath(const QString &file)
void fileChanged(const QString &path)
QHash< QString, int > m_lostFilesAbsenceCounter
KisGroupLayerSP rootLayer() const
void initialRefreshGraph()
void setResolution(double xres, double yres)
QRect extent() const
void convertFromQImage(const QImage &image, const KoColorProfile *profile, qint32 offsetX=0, qint32 offsetY=0)
void makeCloneFrom(KisPaintDeviceSP src, const QRect &rect)
static KisPart * instance()
Definition KisPart.cpp:131
void fileChangedCompressed(bool sync=false)
void fileExistsStateChanged(bool fileExists)
void setPath(const QString &path)
void loadingFinished(KisPaintDeviceSP paintDevice, qreal xRes, qreal yRes, const QSize &size)
void slotFileExistsStateChanged(const QString &path, bool fileExists)
KisSafeDocumentLoader(const QString &path="", QObject *parent=0)
static void log(const QString &message)
Logs with date/time.
@ Read
Definition KoStore.h:29
static KoStore * createStore(const QString &fileName, Mode mode, const QByteArray &appIdentification=QByteArray(), Backend backend=Auto, bool writeMimetype=true)
Definition KoStore.cpp:39
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define dbgKrita
Definition kis_debug.h:45
#define ppVar(var)
Definition kis_debug.h:155
void forceAllDelayedNodesUpdate(KisNodeSP root)
void setUtf8OnStream(QTextStream &stream)
bool addNode(KisNodeSP node, KisNodeSP parent=KisNodeSP(), KisNodeAdditionFlags flags=KisNodeAdditionFlag::None)
KisPaintDeviceSP paintDevice
static KoColorSpaceRegistry * instance()
const KoColorSpace * rgb8(const QString &profileName=QString())