18#include "filemanager.h"
23#include <QVersionNumber>
25#include "fileformat.h"
27#include "layercamera.h"
32 srand(
static_cast<uint
>(time(
nullptr)));
38 dd <<
"\n[Project LOAD diagnostics]\n";
42 handleOpenProjectError(Status::FILE_NOT_FOUND, dd);
48 std::unique_ptr<Object> obj(
new Object);
49 obj->setFilePath(sFileName);
50 obj->createWorkingDir();
56 bool isArchive = isArchiveFormat(sFileName);
58 QString fileFormat =
"Project format: %1";
61 dd << fileFormat.
arg(
".pcl");
65 dd <<
"Failed to copy main xml file";
67 Status st =
copyDir(sFileName +
"." + PFF_OLD_DATA_DIR, strDataFolder);
70 dd.collect(st.details());
75 QString workingDirPath = obj->workingDir();
77 dd <<
QString(
"Working dir: %1").
arg(workingDirPath);
78 dd << fileFormat.
arg(
".pclx");
80 Status sanityCheck = MiniZ::sanityCheck(sFileName);
83 if (!sanityCheck.ok()) {
84 dd.collect(sanityCheck.details());
85 dd <<
"\nError: Unable to extract project, miniz sanity check failed.";
86 handleOpenProjectError(Status::ERROR_INVALID_XML_FILE, dd);
89 Status unzipStatus = unzip(sFileName, workingDirPath);
90 dd.collect(unzipStatus.details());
92 if(unzipStatus.ok()) {
93 dd <<
QString(
"Unzipped at: %1 ").
arg(workingDirPath);
95 dd <<
QString(
"Error: Unzipping failed: %1 ").
arg(workingDirPath);
96 handleOpenProjectError(Status::ERROR_INVALID_XML_FILE, dd);
102 obj->setDataDir(strDataFolder);
103 obj->setMainXMLFile(strMainXMLFile);
106 mMaxProgressValue = totalFileCount;
107 emit progressRangeChanged(mMaxProgressValue);
109 QFile file(strMainXMLFile);
112 dd <<
"Error: No main XML exists!";
113 handleOpenProjectError(Status::ERROR_INVALID_XML_FILE, dd);
118 dd <<
"Error: Main XML file is read only!";
119 handleOpenProjectError(Status::ERROR_FILE_CANNOT_OPEN, dd);
123 dd <<
"Main XML exists: Yes";
128 FILEMANAGER_LOG(
"Couldn't open the main XML file");
129 dd <<
"Error: Unable to parse or open the main XML file";
130 handleOpenProjectError(Status::ERROR_INVALID_XML_FILE, dd);
135 if (!(type.
name() ==
"PencilDocument" || type.
name() ==
"MyObject"))
137 FILEMANAGER_LOG(
"Invalid main XML doctype");
139 handleOpenProjectError(Status::ERROR_INVALID_PENCIL_FILE, dd);
146 dd <<
"Error: Main XML root node is null";
147 handleOpenProjectError(Status::ERROR_INVALID_PENCIL_FILE, dd);
151 loadPalette(obj.get());
155 if (root.
tagName() ==
"document")
157 ok = loadObject(obj.get(), root);
159 else if (root.
tagName() ==
"object" || root.
tagName() ==
"MyOject")
161 ok = loadObjectOldWay(obj.get(), root);
167 dd <<
"Error: Issue occurred during object loading";
168 handleOpenProjectError(Status::ERROR_INVALID_PENCIL_FILE, dd);
172 verifyObject(obj.get());
174 return obj.release();
192 if (element.
tagName() ==
"object")
194 ok =
object->loadXML(element, [
this]{ progressForward(); });
195 if (!ok) FILEMANAGER_LOG(
"Failed to Load object");
198 else if (element.
tagName() ==
"editor" || element.
tagName() ==
"projectdata")
200 object->setData(loadProjectData(element));
202 else if (element.
tagName() ==
"version")
207 if (!fileVersion.
isNull())
209 if (appVersion < fileVersion)
211 qWarning() <<
"You are opening a newer project file in an older version of Pencil2D!";
225 return object->loadXML(root, [
this] { progressForward(); });
228bool FileManager::isArchiveFormat(
const QString& fileName)
const
239 dd <<
"\n[Project SAVE diagnostics]\n";
240 dd << (
"file name:" + sFileName);
242 if (
object ==
nullptr)
244 dd <<
"Error: Object parameter is null";
245 return Status(Status::INVALID_ARGUMENT, dd);
248 dd <<
"Error: File name is empty, unable to save.";
249 return Status(Status::INVALID_ARGUMENT, dd,
250 tr(
"Invalid Save Path"),
251 tr(
"The path is empty."));
254 const int totalCount =
object->totalKeyFrameCount();
255 mMaxProgressValue = totalCount + 5;
256 emit progressRangeChanged(mMaxProgressValue);
261 if (fileInfo.isDir())
263 dd <<
"Error: File name must point to a file, not a directory.";
264 return Status(Status::INVALID_ARGUMENT, dd,
265 tr(
"Invalid Save Path"),
266 tr(
"The path (\"%1\") points to a directory.").arg(fileInfo.absoluteFilePath()));
268 QFileInfo parentDirInfo(fileInfo.dir().absolutePath());
269 if (!parentDirInfo.exists())
271 dd <<
QString(
"Error: The parent directory of %1 does not exist").
arg(sFileName);
272 return Status(Status::INVALID_ARGUMENT, dd,
273 tr(
"Invalid Save Path"),
274 tr(
"The directory (\"%1\") does not exist.").arg(parentDirInfo.absoluteFilePath()));
276 if ((fileInfo.exists() && !fileInfo.isWritable()) || !parentDirInfo.isWritable())
278 dd <<
"Error: File name points to a location that is not writable";
279 return Status(Status::INVALID_ARGUMENT, dd,
280 tr(
"Invalid Save Path"),
281 tr(
"The path (\"%1\") is not writable.").arg(fileInfo.absoluteFilePath()));
289 bool isArchive = isArchiveFormat(sFileName);
292 dd << fileFormat.
arg(
".pcl");
294 sMainXMLFile = sFileName;
295 sDataFolder = sMainXMLFile +
"." + PFF_OLD_DATA_DIR;
299 sTempWorkingFolder =
object->workingDir();
301 dd <<
QString(
"Working dir: %1").
arg(sTempWorkingFolder);
302 dd << fileFormat.
arg(
".pclx");
304 Q_ASSERT(
QDir(sTempWorkingFolder).exists());
306 sMainXMLFile =
QDir(sTempWorkingFolder).
filePath(PFF_XML_FILE_NAME);
307 sDataFolder =
QDir(sTempWorkingFolder).
filePath(PFF_OLD_DATA_DIR);
311 if (!dataInfo.exists())
313 QDir dir(sDataFolder);
315 if (!dir.mkpath(sDataFolder))
317 dd <<
QString(
"Error: Unable to create data directory, tried to save to: %1").
arg(dir.absolutePath());
318 return Status(Status::FAIL, dd,
319 tr(
"Cannot Create Data Directory"),
320 tr(
"Failed to create directory \"%1\". Please make sure you have sufficient permissions.").arg(sDataFolder));
323 if (!dataInfo.isDir())
325 dd <<
QString(
"Error: Expected data to be a directory but found %1 instead").
arg(dataInfo.absoluteFilePath());
326 return Status(Status::FAIL,
328 tr(
"Cannot Create Data Directory"),
329 tr(
"\"%1\" is a file. Please delete the file and try again.").arg(dataInfo.absoluteFilePath()));
333 Status stKeyFrames = writeKeyFrameFiles(
object, sDataFolder, filesToZip);
334 dd.collect(stKeyFrames.details());
336 Status stMainXml = writeMainXml(
object, sMainXMLFile, filesToZip);
337 dd.collect(stMainXml.details());
339 Status stPalette = writePalette(
object, sDataFolder, filesToZip);
340 dd.collect(stPalette.details());
342 const bool saveOk = stKeyFrames.ok() && stMainXml.ok() && stPalette.ok();
348 dd <<
"\n[Archiving diagnostics]\n";
349 QString sBackupFile = backupPreviousFile(sFileName);
352 dd <<
QString(
"\nNote: A backup has been made here: %1").
arg(sBackupFile);
356 return Status(Status::FAIL, dd,
357 tr(
"Internal Error"),
358 tr(
"An internal error occurred. The project could not be saved."));
361 dd <<
"Miniz: Zipping...";
362 Status stMiniz = MiniZ::compressFolder(sFileName, sTempWorkingFolder, filesToZip,
"application/x-pencil2d-pclx");
365 dd.collect(stMiniz.details());
366 dd <<
"\nError: Miniz failed to zip project";
367 return Status(Status::ERROR_MINIZ_FAIL, dd,
369 tr(
"An internal error occurred. The project may not have been saved successfully."));
371 dd <<
"Miniz: Zip file saved successfully";
372 Q_ASSERT(stMiniz.ok());
375 dd <<
"Project saved successfully, deleting backup";
376 deleteBackupFile(sBackupFile);
384 return Status(Status::FAIL, dd,
385 tr(
"Internal Error"),
386 tr(
"An internal error occurred. The project may not have been saved successfully."));
392Status FileManager::writeToWorkingFolder(
const Object*
object)
398 const QString dataFolder =
object->dataDir();
399 const QString mainXml =
object->mainXMLFile();
401 Status stKeyFrames = writeKeyFrameFiles(
object, dataFolder, filesWritten);
402 dd.collect(stKeyFrames.details());
404 Status stMainXml = writeMainXml(
object, mainXml, filesWritten);
405 dd.collect(stMainXml.details());
407 Status stPalette = writePalette(
object, dataFolder, filesWritten);
408 dd.collect(stPalette.details());
410 const bool saveOk = stKeyFrames.ok() && stMainXml.ok() && stPalette.ok();
411 const auto errorCode = (saveOk) ? Status::OK : Status::FAIL;
412 return Status(errorCode, dd);
433 extractProjectData(element, data);
446 currentFrameTag.
setAttribute(
"value", data->getCurrentFrame());
451 QColor color = data->getCurrentColor();
460 currentLayerTag.
setAttribute(
"value", data->getCurrentLayer());
481 tagIsLoop.
setAttribute(
"value", data->isLooping() ?
"true" :
"false");
485 tagRangedPlayback.
setAttribute(
"value", data->isRangedPlayback() ?
"true" :
"false");
489 tagMarkInFrame.
setAttribute(
"value", data->getMarkInFrameNumber());
493 tagMarkOutFrame.
setAttribute(
"value", data->getMarkOutFrameNumber());
502 if (strName ==
"currentFrame")
506 else if (strName ==
"currentColor")
513 data.setCurrentColor(
QColor(r, g, b, a));
515 else if (strName ==
"currentLayer")
519 else if (strName ==
"currentView")
528 data.setCurrentView(
QTransform(m11, m12, m21, m22, dx, dy));
530 else if (strName ==
"fps" || strName ==
"currentFps")
534 else if (strName ==
"isLoop")
536 data.setLooping(element.
attribute(
"value",
"false") ==
"true");
538 else if (strName ==
"isRangedPlayback")
540 data.setRangedPlayback((element.
attribute(
"value",
"false") ==
"true"));
542 else if (strName ==
"markInFrame")
544 data.setMarkInFrameNumber(element.
attribute(
"value",
"0").
toInt());
546 else if (strName ==
"markOutFrame")
548 data.setMarkOutFrameNumber(element.
attribute(
"value",
"15").
toInt());
552void FileManager::handleOpenProjectError(Status::ErrorCode error,
const DebugDetails& dd)
554 QString title =
tr(
"Could not open file");
557 "<li><a href=\"https://discuss.pencil2d.org/c/bugs\">Pencil2D Forum</a></li>"
558 "<li><a href=\"https://github.com/pencil2d/pencil/issues/new\">Github</a></li>"
559 "<li><a href=\"https://discord.gg/8FxdV2g\">Discord<\a></li>"
562 if (error == Status::FILE_NOT_FOUND)
564 errorDesc =
tr(
"The file does not exist, so we are unable to open it."
565 "Please check to make sure the path is correct and try again.");
567 else if (error == Status::ERROR_FILE_CANNOT_OPEN)
569 errorDesc =
tr(
"No permission to read the file. "
570 "Please check you have read permissions for this file and try again.");
575 errorDesc =
tr(
"There was an error processing your file. "
576 "This usually means that your project has been at least partially corrupted. "
577 "Try again with a newer version of Pencil2D, "
578 "or try to use a backup file if you have one. "
579 "If you contact us through one of our official channels we may be able to help you."
580 "For reporting issues, the best places to reach us are:");
583 mError =
Status(error, dd, title, errorDesc + contactLinks);
584 removePFFTmpDirectory(mstrLastTempFolder);
587int FileManager::countExistingBackups(
const QString& fileName)
const
590 QDir directory(fileInfo.absoluteDir());
591 const QString& baseFileName = fileInfo.completeBaseName();
594 for (
const QFileInfo &dirFileInfo : directory.entryInfoList(QDir::Filter::Files)) {
595 QString searchFileAbsPath = dirFileInfo.absoluteFilePath();
596 QString searchFileName = dirFileInfo.baseName();
598 bool sameBaseName = baseFileName.
compare(searchFileName) == 0;
599 if (sameBaseName && searchFileAbsPath.
contains(PFF_BACKUP_IDENTIFIER)) {
613 QString baseName = fileInfo.completeBaseName();
615 int backupCount = countExistingBackups(fileName) + 1;
618 QString sBackupFile = baseName +
"." + PFF_BACKUP_IDENTIFIER + countStr +
"." + fileInfo.suffix();
621 bool ok =
QFile::copy(fileInfo.absoluteFilePath(), sBackupFileFullPath);
624 FILEMANAGER_LOG(
"Cannot backup the previous file");
627 return sBackupFileFullPath;
630void FileManager::deleteBackupFile(
const QString& fileName)
638void FileManager::progressForward()
641 emit progressChanged(mCurrentProgress);
644bool FileManager::loadPalette(
Object* obj)
646 FILEMANAGER_LOG(
"Load Palette..");
648 QString paletteFilePath = validateDataPath(PFF_PALETTE_FILE, obj->dataDir());
649 if (paletteFilePath.
isEmpty() || !obj->importPalette(paletteFilePath))
651 obj->loadDefaultPalette();
659 dd <<
"\n[Keyframes WRITE diagnostics]\n";
661 const int numLayers =
object->getLayerCount();
662 dd <<
QString(
"Total layer count: %1").
arg(numLayers);
664 for (
int i = 0; i < numLayers; ++i)
666 Layer* layer =
object->getLayer(i);
667 layer->presave(dataFolder);
670 bool saveLayersOK =
true;
671 for (
int i = 0; i < numLayers; ++i)
673 Layer* layer =
object->getLayer(i);
675 dd <<
QString(
"Layer[%1] = [id=%2, type=%3, name=%4]").
arg(i).
arg(layer->id()).
arg(layer->type()).
arg(layer->name());
677 Status st = layer->save(dataFolder, filesFlushed, [
this] { progressForward(); });
680 saveLayersOK =
false;
681 dd.collect(st.details());
682 dd <<
QString(
"\nError: Failed to save Layer[%1] %2").
arg(i).
arg(layer->name());
688 auto errorCode = (saveLayersOK) ? Status::OK : Status::FAIL;
691 dd <<
"\nAll Layers saved";
693 dd <<
"\nError: Unable to save all layers";
696 return Status(errorCode, dd);
702 dd <<
"\n[XML WRITE diagnostics]\n";
704 QFile file(mainXmlPath);
707 dd <<
QString(
"Error: Failed to open Main XML at: %1, \nReason: %2").
arg(mainXmlPath).
arg(file.errorString());
708 return Status(Status::ERROR_FILE_CANNOT_OPEN, dd);
723 QDomElement projDataXml = saveProjectData(object->data(), xmlDoc);
727 QDomElement objectElement =
object->saveXML(xmlDoc);
735 dd <<
"Writing main xml file...";
737 const int indentSize = 2;
740 xmlDoc.
save(out, indentSize);
743 dd <<
"Done writing main xml file: " << mainXmlPath;
745 filesWritten.
append(mainXmlPath);
746 return Status(Status::OK, dd);
751 const QString paletteFile =
object->savePalette(dataFolder);
755 dd <<
"\nError: Failed to save palette";
756 return Status(Status::FAIL, dd);
758 filesWritten.
append(paletteFile);
777 if (!src.
exists())
return Status::FILE_NOT_FOUND;
784 dd << (
"Failed to create target directory: " + dst.
absolutePath());
785 return Status(Status::FAIL, dd);
794 dd.collect(st.details());
803 dd <<
"Failed to copy file"
804 << (
"Source path: " + src.
filePath(fileName))
805 << (
"Destination path: " + dst.
filePath(fileName));
815 return Status(Status::FAIL, dd);
822 removePFFTmpDirectory(strUnzipTarget);
824 Status s = MiniZ::uncompressFolder(strZipFile, strUnzipTarget);
827 mstrLastTempFolder = strUnzipTarget;
834 if (!fileInfo.exists())
846 int curLayer = obj->data()->getCurrentLayer();
847 int maxLayer = obj->getLayerCount();
848 if (curLayer >= maxLayer)
850 obj->data()->setCurrentLayer(maxLayer - 1);
854 std::vector<LayerCamera*> camLayers = obj->getLayersByType<
LayerCamera>();
855 if (camLayers.empty())
857 obj->addNewCameraLayer();
865 bool folderExists = pencil2DTempDir.
cd(
"Pencil2D");
871 const QStringList nameFilter(
"*_" PFF_TMP_DECOMPRESS_EXT
"_*");
875 for (
const QString& path : entries)
878 if (isProjectRecoverable(fullPath))
880 qDebug() <<
"Found debris at" << fullPath;
881 recoverables.
append(fullPath);
887bool FileManager::isProjectRecoverable(
const QString& projectFolder)
889 QDir dir(projectFolder);
890 if (!dir.exists()) {
return false; }
893 if (!dir.exists(
"data")) {
return false; }
895 bool ok = dir.cd(
"data");
899 nameFiler <<
"*.png" <<
"*.vec" <<
"*.xml";
902 return (entries.
size() > 0);
905Object* FileManager::recoverUnsavedProject(
QString intermeidatePath)
907 qDebug() <<
"TODO: recover project" << intermeidatePath;
909 QDir projectDir(intermeidatePath);
910 const QString mainXMLPath = projectDir.filePath(PFF_XML_FILE_NAME);
911 const QString dataFolder = projectDir.filePath(PFF_DATA_DIR);
913 std::unique_ptr<Object> object(
new Object);
914 object->setWorkingDir(intermeidatePath);
915 object->setMainXMLFile(mainXMLPath);
916 object->setDataDir(dataFolder);
918 Status st = recoverObject(
object.get());
925 return object.release();
931 bool mainXmlOK =
true;
933 QFile file(object->mainXMLFile());
934 mainXmlOK &= file.exists();
942 mainXmlOK &= (type.
name() ==
"PencilDocument" || type.
name() ==
"MyObject");
945 mainXmlOK &= (!root.
isNull());
948 mainXmlOK &= (!objectTag.
isNull());
950 if (mainXmlOK ==
false)
956 QFile file(object->mainXMLFile());
964 bool ok = loadObject(
object, root);
965 verifyObject(
object);
967 return ok ? Status::OK : Status::FAIL;
973 QDir dataDir(object->dataDir());
976 nameFiler <<
"*.png" <<
"*.vec";
982 for (
const QString& s : entries)
984 int layerIndex = layerIndexFromFilename(s);
987 keyFrameGroups[layerIndex].append(s);
992 const QString mainXMLPath =
object->mainXMLFile();
993 QFile file(mainXMLPath);
996 return Status::ERROR_FILE_CANNOT_OPEN;
1006 QDomElement projDataXml = saveProjectData(object->data(), xmlDoc);
1013 for (
const int layerIndex : keyFrameGroups.
keys())
1020 xmlDoc.
save(fout, 2);
1035 const int layerIndex,
1038 Q_ASSERT(frames.
length() > 0);
1040 Layer::LAYER_TYPE type = frames[0].
endsWith(
".png") ? Layer::BITMAP : Layer::VECTOR;
1044 elemLayer.
setAttribute(
"name", recoverLayerName(type, layerIndex));
1049 for (
const QString& s : frames)
1051 const int framePos = framePosFromFilename(s);
1052 if (framePos < 0) {
continue; }
1058 if (type == Layer::BITMAP)
1070QString FileManager::recoverLayerName(Layer::LAYER_TYPE type,
int index)
1075 return tr(
"Bitmap Layer %1").
arg(index);
1077 return tr(
"Vector Layer %1").
arg(index);
1079 return tr(
"Sound Layer %1").
arg(index);
1086int FileManager::layerIndexFromFilename(
const QString& filename)
1089 if (tokens.
length() >= 3)
1091 return tokens[0].toInt();
1096int FileManager::framePosFromFilename(
const QString& filename)
1099 if (tokens.
length() >= 3)
1101 return tokens[1].toInt();
Status rebuildMainXML(Object *object)
Create a new main.xml based on the png/vec filenames left in the data folder.
Status copyDir(const QDir src, const QDir dst)
Copy a directory to another directory recursively (depth-first).
Status rebuildLayerXmlTag(QDomDocument &doc, QDomElement &elemObject, const int layerIndex, const QStringList &frames)
Rebuild a layer xml tag.
QString absolutePath() const const
bool cd(const QString &dirName)
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
bool exists() const const
QString filePath(const QString &fileName) const const
bool mkpath(const QString &dirPath) const const
QDomElement createElement(const QString &tagName)
QDomProcessingInstruction createProcessingInstruction(const QString &target, const QString &data)
QDomText createTextNode(const QString &value)
QDomDocumentType doctype() const const
QDomElement documentElement() const const
bool setContent(const QByteArray &data, bool namespaceProcessing, QString *errorMsg, int *errorLine, int *errorColumn)
QString name() const const
QString attribute(const QString &name, const QString &defValue) const const
void setAttribute(const QString &name, const QString &value)
QString tagName() const const
QString text() const const
QDomNode appendChild(const QDomNode &newChild)
QDomNode firstChild() const const
QDomElement firstChildElement(const QString &tagName) const const
bool isNull() const const
QDomNode nextSibling() const const
void save(QTextStream &stream, int indent, QDomNode::EncodingPolicy encodingPolicy) const const
QDomElement toElement() const const
bool copy(const QString &newName)
bool exists() const const
virtual bool open(QIODevice::OpenMode mode) override
void append(const T &value)
bool endsWith(const T &value) const const
QList< Key > keys() const const
const T value(const Key &key, const T &defaultValue) const const
QString tr(const char *sourceText, const char *disambiguation, int n)
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString & append(QChar ch)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
int compare(const QString &other, Qt::CaseSensitivity cs) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString number(int n, int base)
double toDouble(bool *ok) const const
int toInt(bool *ok, int base) const const
QVersionNumber fromString(const QString &string, int *suffixIndex)
bool isNull() const const