18#include "filemanager.h"
22#include <QVersionNumber>
24#include "fileformat.h"
26#include "layercamera.h"
30 srand(
static_cast<uint
>(time(
nullptr)));
39 handleOpenProjectError(Status::FILE_NOT_FOUND, dd);
45 std::unique_ptr<Object> obj(
new Object);
46 obj->setFilePath(sFileName);
47 obj->createWorkingDir();
53 bool isArchive = isArchiveFormat(sFileName);
57 dd <<
"Recognized Old Pencil2D File Format (*.pcl) !";
59 strMainXMLFile = sFileName;
60 strDataFolder = strMainXMLFile +
"." + PFF_OLD_DATA_DIR;
64 dd <<
"Recognized New zipped Pencil2D File Format (*.pclx) !";
66 Status sanityCheck = MiniZ::sanityCheck(sFileName);
69 if (!sanityCheck.ok()) {
70 dd.collect(sanityCheck.details());
72 Status unzipStatus = unzip(sFileName, obj->workingDir());
73 dd.collect(unzipStatus.details());
76 strMainXMLFile =
QDir(obj->workingDir()).
filePath(PFF_XML_FILE_NAME);
77 strDataFolder =
QDir(obj->workingDir()).
filePath(PFF_DATA_DIR);
84 obj->setDataDir(strDataFolder);
85 obj->setMainXMLFile(strMainXMLFile);
88 mMaxProgressValue = totalFileCount;
89 emit progressRangeChanged(mMaxProgressValue);
91 QFile file(strMainXMLFile);
94 dd <<
"Main XML file does not exist";
95 handleOpenProjectError(Status::ERROR_INVALID_XML_FILE, dd);
100 handleOpenProjectError(Status::ERROR_FILE_CANNOT_OPEN, dd);
107 FILEMANAGER_LOG(
"Couldn't open the main XML file");
108 dd <<
"Error parsing or opening the main XML file";
109 handleOpenProjectError(Status::ERROR_INVALID_XML_FILE, dd);
114 if (!(type.
name() ==
"PencilDocument" || type.
name() ==
"MyObject"))
116 FILEMANAGER_LOG(
"Invalid main XML doctype");
118 handleOpenProjectError(Status::ERROR_INVALID_PENCIL_FILE, dd);
125 dd <<
"Main XML root node is null";
126 handleOpenProjectError(Status::ERROR_INVALID_PENCIL_FILE, dd);
130 loadPalette(obj.get());
134 if (root.
tagName() ==
"document")
136 ok = loadObject(obj.get(), root);
138 else if (root.
tagName() ==
"object" || root.
tagName() ==
"MyOject")
140 ok = loadObjectOldWay(obj.get(), root);
146 dd <<
"Issue occurred during object loading";
147 handleOpenProjectError(Status::ERROR_INVALID_PENCIL_FILE, dd);
151 verifyObject(obj.get());
153 return obj.release();
171 if (element.
tagName() ==
"object")
173 ok =
object->loadXML(element, [
this]{ progressForward(); });
174 if (!ok) FILEMANAGER_LOG(
"Failed to Load object");
177 else if (element.
tagName() ==
"editor" || element.
tagName() ==
"projectdata")
179 object->setData(loadProjectData(element));
181 else if (element.
tagName() ==
"version")
186 if (!fileVersion.
isNull())
188 if (appVersion < fileVersion)
190 qWarning() <<
"You are opening a newer project file in an older version of Pencil2D!";
204 return object->loadXML(root, [
this] { progressForward(); });
207bool FileManager::isArchiveFormat(
const QString& fileName)
const
219 dd << (
"sFileName = " + sFileName);
221 if (
object ==
nullptr)
223 dd <<
"Object parameter is null";
224 return Status(Status::INVALID_ARGUMENT, dd);
227 dd <<
"File name is empty";
228 return Status(Status::INVALID_ARGUMENT, dd,
229 tr(
"Invalid Save Path"),
230 tr(
"The path is empty."));
233 const int totalCount =
object->totalKeyFrameCount();
234 mMaxProgressValue = totalCount + 5;
235 emit progressRangeChanged(mMaxProgressValue);
240 if (fileInfo.isDir())
242 dd <<
"FileName points to a directory";
243 return Status(Status::INVALID_ARGUMENT, dd,
244 tr(
"Invalid Save Path"),
245 tr(
"The path (\"%1\") points to a directory.").arg(fileInfo.absoluteFilePath()));
247 QFileInfo parentDirInfo(fileInfo.dir().absolutePath());
248 if (!parentDirInfo.exists())
250 dd <<
"The parent directory of sFileName does not exist";
251 return Status(Status::INVALID_ARGUMENT, dd,
252 tr(
"Invalid Save Path"),
253 tr(
"The directory (\"%1\") does not exist.").arg(parentDirInfo.absoluteFilePath()));
255 if ((fileInfo.exists() && !fileInfo.isWritable()) || !parentDirInfo.isWritable())
257 dd <<
"Filename points to a location that is not writable";
258 return Status(Status::INVALID_ARGUMENT, dd,
259 tr(
"Invalid Save Path"),
260 tr(
"The path (\"%1\") is not writable.").arg(fileInfo.absoluteFilePath()));
267 bool isArchive = isArchiveFormat(sFileName);
270 dd <<
"Old Pencil2D File Format (*.pcl) !";
272 sMainXMLFile = sFileName;
273 sDataFolder = sMainXMLFile +
"." + PFF_OLD_DATA_DIR;
277 dd <<
"New zipped Pencil2D File Format (*.pclx) !";
278 dd.collect(MiniZ::sanityCheck(sFileName).details());
280 sTempWorkingFolder =
object->workingDir();
281 Q_ASSERT(
QDir(sTempWorkingFolder).exists());
282 dd <<
QString(
"TempWorkingFolder = ").
append(sTempWorkingFolder);
284 sMainXMLFile =
QDir(sTempWorkingFolder).
filePath(PFF_XML_FILE_NAME);
285 sDataFolder =
QDir(sTempWorkingFolder).
filePath(PFF_OLD_DATA_DIR);
289 if (!dataInfo.exists())
291 QDir dir(sDataFolder);
293 if (!dir.mkpath(sDataFolder))
295 dd <<
QString(
"dir.absolutePath() = %1").
arg(dir.absolutePath());
296 return Status(Status::FAIL, dd,
297 tr(
"Cannot Create Data Directory"),
298 tr(
"Failed to create directory \"%1\". Please make sure you have sufficient permissions.").arg(sDataFolder));
301 if (!dataInfo.isDir())
303 dd <<
QString(
"dataInfo.absoluteFilePath() = ").
append(dataInfo.absoluteFilePath());
304 return Status(Status::FAIL,
306 tr(
"Cannot Create Data Directory"),
307 tr(
"\"%1\" is a file. Please delete the file and try again.").arg(dataInfo.absoluteFilePath()));
311 Status stKeyFrames = writeKeyFrameFiles(
object, sDataFolder, filesToZip);
312 dd.collect(stKeyFrames.details());
314 Status stMainXml = writeMainXml(
object, sMainXMLFile, filesToZip);
315 dd.collect(stMainXml.details());
317 Status stPalette = writePalette(
object, sDataFolder, filesToZip);
318 dd.collect(stPalette.details());
320 const bool saveOk = stKeyFrames.ok() && stMainXml.ok() && stPalette.ok();
326 QString sBackupFile = backupPreviousFile(sFileName);
329 return Status(Status::FAIL, dd,
330 tr(
"Internal Error"),
331 tr(
"An internal error occurred. Your file may not be saved successfully."));
335 Status stMiniz = MiniZ::compressFolder(sFileName, sTempWorkingFolder, filesToZip,
"application/x-pencil2d-pclx");
338 dd.collect(stMiniz.details());
339 return Status(Status::ERROR_MINIZ_FAIL, dd,
341 tr(
"An internal error occurred. Your file may not be saved successfully."));
343 dd <<
"Zip file saved successfully";
344 Q_ASSERT(stMiniz.ok());
347 deleteBackupFile(sBackupFile);
354 return Status(Status::FAIL, dd,
355 tr(
"Internal Error"),
356 tr(
"An internal error occurred. Your file may not be saved successfully."));
362Status FileManager::writeToWorkingFolder(
const Object*
object)
368 const QString dataFolder =
object->dataDir();
369 const QString mainXml =
object->mainXMLFile();
371 Status stKeyFrames = writeKeyFrameFiles(
object, dataFolder, filesWritten);
372 dd.collect(stKeyFrames.details());
374 Status stMainXml = writeMainXml(
object, mainXml, filesWritten);
375 dd.collect(stMainXml.details());
377 Status stPalette = writePalette(
object, dataFolder, filesWritten);
378 dd.collect(stPalette.details());
380 const bool saveOk = stKeyFrames.ok() && stMainXml.ok() && stPalette.ok();
381 const auto errorCode = (saveOk) ? Status::OK : Status::FAIL;
382 return Status(errorCode, dd);
403 extractProjectData(element, data);
416 currentFrameTag.
setAttribute(
"value", data->getCurrentFrame());
421 QColor color = data->getCurrentColor();
430 currentLayerTag.
setAttribute(
"value", data->getCurrentLayer());
451 tagIsLoop.
setAttribute(
"value", data->isLooping() ?
"true" :
"false");
455 tagRangedPlayback.
setAttribute(
"value", data->isRangedPlayback() ?
"true" :
"false");
459 tagMarkInFrame.
setAttribute(
"value", data->getMarkInFrameNumber());
463 tagMarkOutFrame.
setAttribute(
"value", data->getMarkOutFrameNumber());
472 if (strName ==
"currentFrame")
476 else if (strName ==
"currentColor")
483 data.setCurrentColor(
QColor(r, g, b, a));
485 else if (strName ==
"currentLayer")
489 else if (strName ==
"currentView")
498 data.setCurrentView(
QTransform(m11, m12, m21, m22, dx, dy));
500 else if (strName ==
"fps" || strName ==
"currentFps")
504 else if (strName ==
"isLoop")
506 data.setLooping(element.
attribute(
"value",
"false") ==
"true");
508 else if (strName ==
"isRangedPlayback")
510 data.setRangedPlayback((element.
attribute(
"value",
"false") ==
"true"));
512 else if (strName ==
"markInFrame")
514 data.setMarkInFrameNumber(element.
attribute(
"value",
"0").
toInt());
516 else if (strName ==
"markOutFrame")
518 data.setMarkOutFrameNumber(element.
attribute(
"value",
"15").
toInt());
522void FileManager::handleOpenProjectError(Status::ErrorCode error,
const DebugDetails& dd)
524 QString title =
tr(
"Could not open file");
527 "<li><a href=\"https://discuss.pencil2d.org/c/bugs\">Pencil2D Forum</a></li>"
528 "<li><a href=\"https://github.com/pencil2d/pencil/issues/new\">Github</a></li>"
529 "<li><a href=\"https://discord.gg/8FxdV2g\">Discord<\a></li>"
532 if (error == Status::FILE_NOT_FOUND)
534 errorDesc =
tr(
"The file does not exist, so we are unable to open it."
535 "Please check to make sure the path is correct and try again.");
537 else if (error == Status::ERROR_FILE_CANNOT_OPEN)
539 errorDesc =
tr(
"No permission to read the file. "
540 "Please check you have read permissions for this file and try again.");
545 errorDesc =
tr(
"There was an error processing your file. "
546 "This usually means that your project has been at least partially corrupted. "
547 "Try again with a newer version of Pencil2D, "
548 "or try to use a backup file if you have one. "
549 "If you contact us through one of our official channels we may be able to help you."
550 "For reporting issues, the best places to reach us are:");
553 mError =
Status(error, dd, title, errorDesc + contactLinks);
554 removePFFTmpDirectory(mstrLastTempFolder);
557int FileManager::countExistingBackups(
const QString& fileName)
const
560 QDir directory(fileInfo.absoluteDir());
561 const QString& baseName = fileInfo.completeBaseName();
564 for (
QFileInfo dirFileInfo : directory.entryInfoList(QDir::Filter::Files)) {
565 QString searchFileBaseName = dirFileInfo.completeBaseName();
566 if (baseName.
compare(searchFileBaseName) == 0 && searchFileBaseName.
contains(PFF_BACKUP_IDENTIFIER)) {
580 QString baseName = fileInfo.completeBaseName();
582 int backupCount = countExistingBackups(fileName) + 1;
585 QString sBackupFile = baseName +
"." + PFF_BACKUP_IDENTIFIER + countStr +
"." + fileInfo.suffix();
588 bool ok =
QFile::copy(fileInfo.absoluteFilePath(), sBackupFileFullPath);
591 FILEMANAGER_LOG(
"Cannot backup the previous file");
594 return sBackupFileFullPath;
597void FileManager::deleteBackupFile(
const QString& fileName)
605void FileManager::progressForward()
608 emit progressChanged(mCurrentProgress);
611bool FileManager::loadPalette(
Object* obj)
613 FILEMANAGER_LOG(
"Load Palette..");
616 if (!obj->importPalette(paletteFilePath))
618 obj->loadDefaultPalette();
627 const int numLayers =
object->getLayerCount();
628 dd <<
QString(
"Total %1 layers").
arg(numLayers);
630 for (
int i = 0; i < numLayers; ++i)
632 Layer* layer =
object->getLayer(i);
633 layer->presave(dataFolder);
636 bool saveLayersOK =
true;
637 for (
int i = 0; i < numLayers; ++i)
639 Layer* layer =
object->getLayer(i);
641 dd <<
QString(
"Layer[%1] = [id=%2, type=%3, name=%4]").
arg(i).
arg(layer->id()).
arg(layer->type()).
arg(layer->name());
643 Status st = layer->save(dataFolder, filesFlushed, [
this] { progressForward(); });
646 saveLayersOK =
false;
647 dd.collect(st.details());
648 dd <<
QString(
" !! Failed to save Layer[%1] %2").
arg(i).
arg(layer->name());
651 dd <<
"All Layers saved";
655 auto errorCode = (saveLayersOK) ? Status::OK : Status::FAIL;
656 return Status(errorCode, dd);
663 QFile file(mainXmlPath);
666 dd <<
"Failed to open Main XML" << mainXmlPath;
667 return Status(Status::ERROR_FILE_CANNOT_OPEN, dd);
679 QDomElement projDataXml = saveProjectData(object->data(), xmlDoc);
683 QDomElement objectElement =
object->saveXML(xmlDoc);
691 dd <<
"Writing main xml file...";
693 const int indentSize = 2;
696 xmlDoc.
save(out, indentSize);
700 dd <<
"Done writing main xml file: " << mainXmlPath;
702 filesWritten.
append(mainXmlPath);
703 return Status(Status::OK, dd);
708 const QString paletteFile =
object->savePalette(dataFolder);
712 dd <<
"Failed to save palette";
713 return Status(Status::FAIL, dd);
715 filesWritten.
append(paletteFile);
722 removePFFTmpDirectory(strUnzipTarget);
724 Status s = MiniZ::uncompressFolder(strZipFile, strUnzipTarget);
727 mstrLastTempFolder = strUnzipTarget;
734 if (!fileInfo.exists())
746 int curLayer = obj->data()->getCurrentLayer();
747 int maxLayer = obj->getLayerCount();
748 if (curLayer >= maxLayer)
750 obj->data()->setCurrentLayer(maxLayer - 1);
754 std::vector<LayerCamera*> camLayers = obj->getLayersByType<
LayerCamera>();
755 if (camLayers.empty())
757 obj->addNewCameraLayer();
765 bool folderExists = pencil2DTempDir.
cd(
"Pencil2D");
771 const QStringList nameFilter(
"*_" PFF_TMP_DECOMPRESS_EXT
"_*");
775 for (
const QString& path : entries)
778 if (isProjectRecoverable(fullPath))
780 qDebug() <<
"Found debris at" << fullPath;
781 recoverables.
append(fullPath);
787bool FileManager::isProjectRecoverable(
const QString& projectFolder)
789 QDir dir(projectFolder);
790 if (!dir.exists()) {
return false; }
793 if (!dir.exists(
"data")) {
return false; }
795 bool ok = dir.cd(
"data");
799 nameFiler <<
"*.png" <<
"*.vec" <<
"*.xml";
802 return (entries.
size() > 0);
805Object* FileManager::recoverUnsavedProject(
QString intermeidatePath)
807 qDebug() <<
"TODO: recover project" << intermeidatePath;
809 QDir projectDir(intermeidatePath);
810 const QString mainXMLPath = projectDir.filePath(PFF_XML_FILE_NAME);
811 const QString dataFolder = projectDir.filePath(PFF_DATA_DIR);
813 std::unique_ptr<Object> object(
new Object);
814 object->setWorkingDir(intermeidatePath);
815 object->setMainXMLFile(mainXMLPath);
816 object->setDataDir(dataFolder);
818 Status st = recoverObject(
object.get());
825 return object.release();
831 bool mainXmlOK =
true;
833 QFile file(object->mainXMLFile());
834 mainXmlOK &= file.exists();
842 mainXmlOK &= (type.
name() ==
"PencilDocument" || type.
name() ==
"MyObject");
845 mainXmlOK &= (!root.
isNull());
848 mainXmlOK &= (!objectTag.
isNull());
850 if (mainXmlOK ==
false)
856 QFile file(object->mainXMLFile());
864 bool ok = loadObject(
object, root);
865 verifyObject(
object);
867 return ok ? Status::OK : Status::FAIL;
873 QDir dataDir(object->dataDir());
876 nameFiler <<
"*.png" <<
"*.vec";
882 for (
const QString& s : entries)
884 int layerIndex = layerIndexFromFilename(s);
887 keyFrameGroups[layerIndex].append(s);
892 const QString mainXMLPath =
object->mainXMLFile();
893 QFile file(mainXMLPath);
896 return Status::ERROR_FILE_CANNOT_OPEN;
906 QDomElement projDataXml = saveProjectData(object->data(), xmlDoc);
913 for (
const int layerIndex : keyFrameGroups.
keys())
920 xmlDoc.
save(fout, 2);
936 const int layerIndex,
939 Q_ASSERT(frames.
length() > 0);
941 Layer::LAYER_TYPE type = frames[0].
endsWith(
".png") ? Layer::BITMAP : Layer::VECTOR;
945 elemLayer.
setAttribute(
"name", recoverLayerName(type, layerIndex));
950 for (
const QString& s : frames)
952 const int framePos = framePosFromFilename(s);
953 if (framePos < 0) {
continue; }
959 if (type == Layer::BITMAP)
971QString FileManager::recoverLayerName(Layer::LAYER_TYPE type,
int index)
976 return tr(
"Bitmap Layer %1").
arg(index);
978 return tr(
"Vector Layer %1").
arg(index);
980 return tr(
"Sound Layer %1").
arg(index);
987int FileManager::layerIndexFromFilename(
const QString& filename)
992 return tokens[0].toInt();
997int FileManager::framePosFromFilename(
const QString& filename)
1000 if (tokens.
length() >= 3)
1002 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 rebuildLayerXmlTag(QDomDocument &doc, QDomElement &elemObject, const int layerIndex, const QStringList &frames)
Rebuild a layer xml tag.
bool cd(const QString &dirName)
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
QString filePath(const QString &fileName) 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
virtual void close() 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