18 #include "filemanager.h"
22 #include "pencildef.h"
24 #include "fileformat.h"
26 #include "layercamera.h"
31 QString openErrorDesc =
QObject::tr(
"There was an error processing your file. This usually means that your project has "
32 "been at least partially corrupted. You can try again with a newer version of Pencil2D, "
33 "or you can try to use a backup file if you have one. If you contact us through one of "
34 "our official channels we may be able to help you. For reporting issues, "
35 "the best places to reach us are:");
37 "<li><a href=\"https://discuss.pencil2d.org/c/bugs\">Pencil2D Forum</a></li>"
38 "<li><a href=\"https://github.com/pencil2d/pencil/issues/new\">Github</a></li>"
39 "<li><a href=\"https://discord.gg/8FxdV2g\">Discord<\a></li>"
45 srand(static_cast<uint>(time(
nullptr)));
54 FILEMANAGER_LOG(
"ERROR - File doesn't exist");
55 return cleanUpWithErrorCode(
Status(Status::FILE_NOT_FOUND, dd,
tr(
"Could not open file"),
56 tr(
"The file does not exist, so we are unable to open it. Please check "
57 "to make sure the path is correct and that the file is accessible and try again.")));
62 std::unique_ptr<Object> obj(
new Object);
63 obj->setFilePath(sFileName);
64 obj->createWorkingDir();
70 bool oldFormat = isOldForamt(sFileName);
71 dd <<
QString(
"Is old format: ").
append(oldFormat ?
"true" :
"false");
75 dd <<
"Recognized Old Pencil2D File Format (*.pcl) !";
77 strMainXMLFile = sFileName;
78 strDataFolder = strMainXMLFile +
"." + PFF_OLD_DATA_DIR;
82 dd <<
"Recognized New zipped Pencil2D File Format (*.pclx) !";
84 unzip(sFileName, obj->workingDir());
86 strMainXMLFile =
QDir(obj->workingDir()).filePath(PFF_XML_FILE_NAME);
87 strDataFolder =
QDir(obj->workingDir()).filePath(PFF_DATA_DIR);
94 obj->setDataDir(strDataFolder);
95 obj->setMainXMLFile(strMainXMLFile);
98 mMaxProgressValue = totalFileCount;
99 emit progressRangeChanged(mMaxProgressValue);
101 QFile file(strMainXMLFile);
104 dd <<
"Main XML file does not exist";
105 return cleanUpWithErrorCode(
Status(Status::ERROR_INVALID_XML_FILE, dd, openErrorTitle, openErrorDesc + contactLinks));
109 return cleanUpWithErrorCode(
Status(Status::ERROR_FILE_CANNOT_OPEN, dd,
tr(
"Could not open file"),
110 tr(
"This program does not have permission to read the file you have selected. "
111 "Please check that you have read permissions for this file and try again.")));
117 FILEMANAGER_LOG(
"Couldn't open the main XML file");
118 dd <<
"Error parsing or opening the main XML file";
119 return cleanUpWithErrorCode(
Status(Status::ERROR_INVALID_XML_FILE, dd, openErrorTitle, openErrorDesc + contactLinks));
123 if (!(type.
name() ==
"PencilDocument" || type.
name() ==
"MyObject"))
125 FILEMANAGER_LOG(
"Invalid main XML doctype");
127 return cleanUpWithErrorCode(
Status(Status::ERROR_INVALID_PENCIL_FILE, dd, openErrorTitle, openErrorDesc + contactLinks));
133 dd <<
"Main XML root node is null";
134 return cleanUpWithErrorCode(
Status(Status::ERROR_INVALID_PENCIL_FILE, dd, openErrorTitle, openErrorDesc + contactLinks));
137 loadPalette(obj.get());
141 if (root.
tagName() ==
"document")
143 ok = loadObject(obj.get(), root);
145 else if (root.
tagName() ==
"object" || root.
tagName() ==
"MyOject")
147 ok = loadObjectOldWay(obj.get(), root);
153 dd <<
"Issue occurred during object loading";
154 return cleanUpWithErrorCode(
Status(Status::ERROR_INVALID_PENCIL_FILE, dd,
""));
157 verifyObject(obj.get());
159 return obj.release();
177 if (element.
tagName() ==
"object")
179 ok =
object->loadXML(element, [
this]{ progressForward(); });
180 if (!ok) FILEMANAGER_LOG(
"Failed to Load object");
183 else if (element.
tagName() ==
"editor" || element.
tagName() ==
"projectdata")
185 ObjectData* projectData = loadProjectData(element);
186 object->setData(projectData);
198 return object->loadXML(root, [
this] { progressForward(); });
201 bool FileManager::isOldForamt(
const QString& fileName)
const
203 return !(MiniZ::isZip(fileName));
210 dd << (
"sFileName = " + sFileName);
212 if (
object ==
nullptr)
214 dd <<
"object parameter is null";
215 return Status(Status::INVALID_ARGUMENT, dd);
218 const int totalCount =
object->totalKeyFrameCount();
219 mMaxProgressValue = totalCount + 5;
220 emit progressRangeChanged(mMaxProgressValue);
225 if (fileInfo.isDir())
227 dd <<
"FileName points to a directory";
228 return Status(Status::INVALID_ARGUMENT, dd,
229 tr(
"Invalid Save Path"),
230 tr(
"The path (\"%1\") points to a directory.").arg(fileInfo.absoluteFilePath()));
232 QFileInfo parentDirInfo(fileInfo.dir().absolutePath());
233 if (!parentDirInfo.exists())
235 dd <<
"The parent directory of sFileName does not exist";
236 return Status(Status::INVALID_ARGUMENT, dd,
237 tr(
"Invalid Save Path"),
238 tr(
"The directory (\"%1\") does not exist.").arg(parentDirInfo.absoluteFilePath()));
240 if ((fileInfo.exists() && !fileInfo.isWritable()) || !parentDirInfo.isWritable())
242 dd <<
"Filename points to a location that is not writable";
243 return Status(Status::INVALID_ARGUMENT, dd,
244 tr(
"Invalid Save Path"),
245 tr(
"The path (\"%1\") is not writable.").arg(fileInfo.absoluteFilePath()));
252 const bool isOldType = sFileName.
endsWith(PFF_OLD_EXTENSION);
255 dd <<
"Old Pencil2D File Format (*.pcl) !";
257 sMainXMLFile = sFileName;
258 sDataFolder = sMainXMLFile +
"." + PFF_OLD_DATA_DIR;
262 dd <<
"New zipped Pencil2D File Format (*.pclx) !";
264 sTempWorkingFolder =
object->workingDir();
265 Q_ASSERT(
QDir(sTempWorkingFolder).exists());
266 dd <<
QString(
"TempWorkingFolder = ").
append(sTempWorkingFolder);
268 sMainXMLFile =
QDir(sTempWorkingFolder).
filePath(PFF_XML_FILE_NAME);
269 sDataFolder =
QDir(sTempWorkingFolder).
filePath(PFF_OLD_DATA_DIR);
273 if (!dataInfo.exists())
275 QDir dir(sDataFolder);
277 if (!dir.mkpath(sDataFolder))
279 dd <<
QString(
"dir.absolutePath() = %1").
arg(dir.absolutePath());
280 return Status(Status::FAIL, dd,
281 tr(
"Cannot Create Data Directory"),
282 tr(
"Failed to create directory \"%1\". Please make sure you have sufficient permissions.").arg(sDataFolder));
285 if (!dataInfo.isDir())
287 dd <<
QString(
"dataInfo.absoluteFilePath() = ").
append(dataInfo.absoluteFilePath());
288 return Status(Status::FAIL,
290 tr(
"Cannot Create Data Directory"),
291 tr(
"\"%1\" is a file. Please delete the file and try again.").arg(dataInfo.absoluteFilePath()));
295 Status stKeyFrames = writeKeyFrameFiles(
object, sDataFolder, filesToZip);
296 dd.collect(stKeyFrames.details());
298 Status stMainXml = writeMainXml(
object, sMainXMLFile, filesToZip);
299 dd.collect(stMainXml.details());
301 Status stPalette = writePalette(
object, sDataFolder, filesToZip);
302 dd.collect(stPalette.details());
304 const bool saveOk = stKeyFrames.ok() && stMainXml.ok() && stPalette.ok();
312 QString sBackupFile = backupPreviousFile(sFileName);
314 Status stMiniz = MiniZ::compressFolder(sFileName, sTempWorkingFolder, filesToZip);
317 dd.collect(stMiniz.details());
318 return Status(Status::ERROR_MINIZ_FAIL, dd,
320 tr(
"An internal error occurred. Your file may not be saved successfully."));
322 dd <<
"Zip file saved successfully";
323 Q_ASSERT(stMiniz.ok());
326 deleteBackupFile(sBackupFile);
333 return Status(Status::FAIL, dd,
334 tr(
"Internal Error"),
335 tr(
"An internal error occurred. Your file may not be saved successfully."));
341 Status FileManager::writeToWorkingFolder(
const Object*
object)
347 const QString dataFolder =
object->dataDir();
348 const QString mainXml =
object->mainXMLFile();
350 Status stKeyFrames = writeKeyFrameFiles(
object, dataFolder, filesWritten);
351 dd.collect(stKeyFrames.details());
353 Status stMainXml = writeMainXml(
object, mainXml, filesWritten);
354 dd.collect(stMainXml.details());
356 Status stPalette = writePalette(
object, dataFolder, filesWritten);
357 dd.collect(stPalette.details());
359 const bool saveOk = stKeyFrames.ok() && stMainXml.ok() && stPalette.ok();
360 const auto errorCode = (saveOk) ? Status::OK : Status::FAIL;
361 return Status(errorCode, dd);
382 extractProjectData(element, data);
395 currentFrameTag.
setAttribute(
"value", data->getCurrentFrame());
400 QColor color = data->getCurrentColor();
409 currentLayerTag.
setAttribute(
"value", data->getCurrentLayer());
430 tagIsLoop.
setAttribute(
"value", data->isLooping() ?
"true" :
"false");
434 tagRangedPlayback.
setAttribute(
"value", data->isRangedPlayback() ?
"true" :
"false");
438 tagMarkInFrame.
setAttribute(
"value", data->getMarkInFrameNumber());
442 tagMarkOutFrame.
setAttribute(
"value", data->getMarkOutFrameNumber());
453 if (strName ==
"currentFrame")
457 else if (strName ==
"currentColor")
464 data->setCurrentColor(
QColor(r, g, b, a));
466 else if (strName ==
"currentLayer")
470 else if (strName ==
"currentView")
479 data->setCurrentView(
QTransform(m11, m12, m21, m22, dx, dy));
481 else if (strName ==
"fps" || strName ==
"currentFps")
485 else if (strName ==
"isLoop")
487 data->setLooping(element.
attribute(
"value",
"false") ==
"true");
489 else if (strName ==
"isRangedPlayback")
491 data->setRangedPlayback((element.
attribute(
"value",
"false") ==
"true"));
493 else if (strName ==
"markInFrame")
495 data->setMarkInFrameNumber(element.
attribute(
"value",
"0").
toInt());
497 else if (strName ==
"markOutFrame")
499 data->setMarkOutFrameNumber(element.
attribute(
"value",
"15").
toInt());
506 removePFFTmpDirectory(mstrLastTempFolder);
516 QString sBackupFile = info.completeBaseName() +
".backup." + info.suffix();
517 QString sBackupFileFullPath =
QDir(info.absolutePath()).filePath(sBackupFile);
519 bool ok =
QFile::rename(info.absoluteFilePath(), sBackupFileFullPath);
522 FILEMANAGER_LOG(
"Cannot backup the previous file");
525 return sBackupFileFullPath;
528 void FileManager::deleteBackupFile(
const QString& fileName)
536 void FileManager::progressForward()
539 emit progressChanged(mCurrentProgress);
542 bool FileManager::loadPalette(
Object* obj)
544 FILEMANAGER_LOG(
"Load Palette..");
546 QString paletteFilePath =
QDir(obj->dataDir()).filePath(PFF_PALETTE_FILE);
547 if (!obj->importPalette(paletteFilePath))
549 obj->loadDefaultPalette();
558 const int numLayers =
object->getLayerCount();
559 dd <<
QString(
"Total %1 layers").
arg(numLayers);
561 for (
int i = 0; i < numLayers; ++i)
563 Layer* layer =
object->getLayer(i);
564 layer->presave(dataFolder);
567 bool saveLayersOK =
true;
568 for (
int i = 0; i < numLayers; ++i)
570 Layer* layer =
object->getLayer(i);
572 dd <<
QString(
"Layer[%1] = [id=%2, name=%3, type=%4]").
arg(i).
arg(layer->id()).arg(layer->name()).arg(layer->type());
574 Status st = layer->save(dataFolder, filesFlushed, [
this] { progressForward(); });
577 saveLayersOK =
false;
578 dd.collect(st.details());
579 dd <<
QString(
" !! Failed to save Layer[%1] %2").
arg(i).
arg(layer->name());
582 dd <<
"All Layers saved";
586 auto errorCode = (saveLayersOK) ? Status::OK : Status::FAIL;
587 return Status(errorCode, dd);
597 dd <<
"Failed to open Main XML" << mainXml;
598 return Status(Status::ERROR_FILE_CANNOT_OPEN, dd);
610 QDomElement projDataXml = saveProjectData(object->data(), xmlDoc);
614 QDomElement objectElement =
object->saveXML(xmlDoc);
617 dd <<
"Writing main xml file...";
619 const int indentSize = 2;
622 xmlDoc.
save(out, indentSize);
626 dd <<
"Done writing main xml file: " << mainXml;
628 filesWritten.
append(mainXml);
629 return Status(Status::OK, dd);
634 const QString paletteFile =
object->savePalette(dataFolder);
638 dd <<
"Failed to save palette";
639 return Status(Status::FAIL, dd);
641 filesWritten.
append(paletteFile);
645 void FileManager::unzip(
const QString& strZipFile,
const QString& strUnzipTarget)
648 removePFFTmpDirectory(strUnzipTarget);
650 Status s = MiniZ::uncompressFolder(strZipFile, strUnzipTarget);
653 mstrLastTempFolder = strUnzipTarget;
659 if (!fileInfo.exists())
671 int curLayer = obj->data()->getCurrentLayer();
672 int maxLayer = obj->getLayerCount();
673 if (curLayer >= maxLayer)
675 obj->data()->setCurrentLayer(maxLayer - 1);
679 std::vector<LayerCamera*> camLayers = obj->getLayersByType<
LayerCamera>();
680 if (camLayers.empty())
682 obj->addNewCameraLayer();
687 QStringList FileManager::searchForUnsavedProjects()
690 bool folderExists = pencil2DTempDir.
cd(
"Pencil2D");
696 const QStringList nameFilter(
"*_" PFF_TMP_DECOMPRESS_EXT
"_*");
700 for (
const QString path : entries)
703 if (isProjectRecoverable(fullPath))
705 qDebug() <<
"Found debris at" << fullPath;
706 recoverables.
append(fullPath);
712 bool FileManager::isProjectRecoverable(
const QString& projectFolder)
714 QDir dir(projectFolder);
715 if (!dir.exists()) {
return false; }
718 if (!dir.exists(
"data")) {
return false; }
720 bool ok = dir.cd(
"data");
724 nameFiler <<
"*.png" <<
"*.vec" <<
"*.xml";
727 return (entries.
size() > 0);
730 Object* FileManager::recoverUnsavedProject(
QString intermeidatePath)
732 qDebug() <<
"TODO: recover project" << intermeidatePath;
734 QDir projectDir(intermeidatePath);
735 const QString mainXMLPath = projectDir.filePath(PFF_XML_FILE_NAME);
736 const QString dataFolder = projectDir.filePath(PFF_DATA_DIR);
738 std::unique_ptr<Object> object(
new Object);
739 object->setWorkingDir(intermeidatePath);
740 object->setMainXMLFile(mainXMLPath);
741 object->setDataDir(dataFolder);
743 Status st = recoverObject(
object.
get());
750 return object.release();
756 bool mainXmlOK =
true;
758 QFile file(object->mainXMLFile());
759 mainXmlOK &= file.
exists();
767 mainXmlOK &= (type.
name() ==
"PencilDocument" || type.
name() ==
"MyObject");
770 mainXmlOK &= (!root.
isNull());
773 mainXmlOK &= (objectTag.
isNull() ==
false);
775 if (mainXmlOK ==
false)
781 QFile file(object->mainXMLFile());
789 bool ok = loadObject(
object, root);
790 verifyObject(
object);
792 return ok ? Status::OK : Status::FAIL;
798 QDir dataDir(object->dataDir());
801 nameFiler <<
"*.png" <<
"*.vec";
807 for (
const QString& s : entries)
809 int layerIndex = layerIndexFromFilename(s);
812 keyFrameGroups[layerIndex].append(s);
817 const QString mainXMLPath =
object->mainXMLFile();
818 QFile file(mainXMLPath);
821 return Status::ERROR_FILE_CANNOT_OPEN;
831 QDomElement projDataXml = saveProjectData(object->data(), xmlDoc);
838 for (
const int layerIndex : keyFrameGroups.
keys())
845 xmlDoc.
save(fout, 2);
861 const int layerIndex,
864 Q_ASSERT(frames.
length() > 0);
866 Layer::LAYER_TYPE type = frames[0].
endsWith(
".png") ? Layer::BITMAP : Layer::VECTOR;
870 elemLayer.
setAttribute(
"name", recoverLayerName(type, layerIndex));
875 for (
const QString& s : frames)
877 const int framePos = framePosFromFilename(s);
878 if (framePos < 0) {
continue; }
884 if (type == Layer::BITMAP)
896 QString FileManager::recoverLayerName(Layer::LAYER_TYPE type,
int index)
912 int FileManager::layerIndexFromFilename(
const QString& filename)
917 return tokens[0].toInt();
922 int FileManager::framePosFromFilename(
const QString& filename)
927 return tokens[1].toInt();
QString & append(QChar ch)
QDomProcessingInstruction createProcessingInstruction(const QString &target, const QString &data)
QDomNode appendChild(const QDomNode &newChild)
QString attribute(const QString &name, const QString &defValue) const const
bool rename(const QString &newName)
QString filePath(const QString &fileName) const const
bool endsWith(const T &value) const const
QDomElement documentElement() const const
bool exists() const const
QDomDocumentType doctype() const const
double toDouble(bool *ok) const const
QString tr(const char *sourceText, const char *disambiguation, int n)
QString name() const const
QDomNode nextSibling() const const
QDomElement toElement() const const
QList< Key > keys() const const
void append(const T &value)
void setAttribute(const QString &name, const QString &value)
int toInt(bool *ok, int base) const const
bool cd(const QString &dirName)
bool isEmpty() const const
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
Status rebuildLayerXmlTag(QDomDocument &doc, QDomElement &elemObject, const int layerIndex, const QStringList &frames)
Rebuild a layer xml tag.
virtual bool open(QIODevice::OpenMode mode) override
bool isNull() const const
void save(QTextStream &stream, int indent, QDomNode::EncodingPolicy encodingPolicy) const const
QDomNode firstChild() const const
virtual void close() override
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QDomElement firstChildElement(const QString &tagName) const const
QString tagName() const const
QDomElement createElement(const QString &tagName)
Status rebuildMainXML(Object *object)
Create a new main.xml based on the png/vec filenames left in the data folder.
bool setContent(const QByteArray &data, bool namespaceProcessing, QString *errorMsg, int *errorLine, int *errorColumn)
const T value(const Key &key, const T &defaultValue) const const