Pencil2D Animation
Download Community News Docs Contribute

core_lib/src/structure/filemanager.cpp Source File

  • Main Page
  • Related Pages
  • Classes
  • Files
  •  
  • File List
Loading...
Searching...
No Matches
  • core_lib
  • src
  • structure
filemanager.cpp
1/*
2
3Pencil2D - Traditional Animation Software
4Copyright (C) 2005-2007 Patrick Corrieri & Pascal Naidon
5Copyright (C) 2012-2020 Matthew Chiawen Chang
6
7This program is free software; you can redistribute it and/or
8modify it under the terms of the GNU General Public License
9as published by the Free Software Foundation; version 2 of the License.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16*/
17
18#include "filemanager.h"
19
20#include <ctime>
21#include <QDir>
22#include <QVersionNumber>
23#include "qminiz.h"
24#include "fileformat.h"
25#include "object.h"
26#include "layercamera.h"
27
28FileManager::FileManager(QObject* parent) : QObject(parent)
29{
30 srand(static_cast<uint>(time(nullptr)));
31}
32
33Object* FileManager::load(const QString& sFileName)
34{
35 DebugDetails dd;
36 dd << QString("File name: ").append(sFileName);
37 if (!QFile::exists(sFileName))
38 {
39 handleOpenProjectError(Status::FILE_NOT_FOUND, dd);
40 return nullptr;
41 }
42
43 progressForward();
44
45 std::unique_ptr<Object> obj(new Object);
46 obj->setFilePath(sFileName);
47 obj->createWorkingDir();
48
49 QString strMainXMLFile;
50 QString strDataFolder;
51
52 // Test file format: new zipped .pclx or old .pcl?
53 bool isArchive = isArchiveFormat(sFileName);
54
55 if (!isArchive)
56 {
57 dd << "Recognized Old Pencil2D File Format (*.pcl) !";
58
59 strMainXMLFile = sFileName;
60 strDataFolder = strMainXMLFile + "." + PFF_OLD_DATA_DIR;
61 }
62 else
63 {
64 dd << "Recognized New zipped Pencil2D File Format (*.pclx) !";
65
66 Status sanityCheck = MiniZ::sanityCheck(sFileName);
67
68 // Let's check if we can read the file before we try to unzip.
69 if (!sanityCheck.ok()) {
70 dd.collect(sanityCheck.details());
71 } else {
72 Status unzipStatus = unzip(sFileName, obj->workingDir());
73 dd.collect(unzipStatus.details());
74 }
75
76 strMainXMLFile = QDir(obj->workingDir()).filePath(PFF_XML_FILE_NAME);
77 strDataFolder = QDir(obj->workingDir()).filePath(PFF_DATA_DIR);
78 }
79
80 dd << QString("XML file: ").append(strMainXMLFile)
81 << QString("Data folder: ").append(strDataFolder)
82 << QString("Working folder: ").append(obj->workingDir());
83
84 obj->setDataDir(strDataFolder);
85 obj->setMainXMLFile(strMainXMLFile);
86
87 int totalFileCount = QDir(strDataFolder).entryList(QDir::Files).size();
88 mMaxProgressValue = totalFileCount;
89 emit progressRangeChanged(mMaxProgressValue);
90
91 QFile file(strMainXMLFile);
92 if (!file.exists())
93 {
94 dd << "Main XML file does not exist";
95 handleOpenProjectError(Status::ERROR_INVALID_XML_FILE, dd);
96 return nullptr;
97 }
98 if (!file.open(QFile::ReadOnly))
99 {
100 handleOpenProjectError(Status::ERROR_FILE_CANNOT_OPEN, dd);
101 return nullptr;
102 }
103
104 QDomDocument xmlDoc;
105 if (!xmlDoc.setContent(&file))
106 {
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);
110 return nullptr;
111 }
112
113 QDomDocumentType type = xmlDoc.doctype();
114 if (!(type.name() == "PencilDocument" || type.name() == "MyObject"))
115 {
116 FILEMANAGER_LOG("Invalid main XML doctype");
117 dd << QString("Invalid main XML doctype: ").append(type.name());
118 handleOpenProjectError(Status::ERROR_INVALID_PENCIL_FILE, dd);
119 return nullptr;
120 }
121
122 QDomElement root = xmlDoc.documentElement();
123 if (root.isNull())
124 {
125 dd << "Main XML root node is null";
126 handleOpenProjectError(Status::ERROR_INVALID_PENCIL_FILE, dd);
127 return nullptr;
128 }
129
130 loadPalette(obj.get());
131
132 bool ok = true;
133
134 if (root.tagName() == "document")
135 {
136 ok = loadObject(obj.get(), root);
137 }
138 else if (root.tagName() == "object" || root.tagName() == "MyOject") // old Pencil format (<=0.4.3)
139 {
140 ok = loadObjectOldWay(obj.get(), root);
141 }
142
143 if (!ok)
144 {
145 obj.reset();
146 dd << "Issue occurred during object loading";
147 handleOpenProjectError(Status::ERROR_INVALID_PENCIL_FILE, dd);
148 return nullptr;
149 }
150
151 verifyObject(obj.get());
152
153 return obj.release();
154}
155
156bool FileManager::loadObject(Object* object, const QDomElement& root)
157{
158 QDomElement e = root.firstChildElement("object");
159 if (e.isNull())
160 return false;
161
162 bool ok = true;
163 for (QDomNode node = root.firstChild(); !node.isNull(); node = node.nextSibling())
164 {
165 QDomElement element = node.toElement(); // try to convert the node to an element.
166 if (element.isNull())
167 {
168 continue;
169 }
170
171 if (element.tagName() == "object")
172 {
173 ok = object->loadXML(element, [this]{ progressForward(); });
174 if (!ok) FILEMANAGER_LOG("Failed to Load object");
175
176 }
177 else if (element.tagName() == "editor" || element.tagName() == "projectdata")
178 {
179 object->setData(loadProjectData(element));
180 }
181 else if (element.tagName() == "version")
182 {
183 QVersionNumber fileVersion = QVersionNumber::fromString(element.text());
184 QVersionNumber appVersion = QVersionNumber::fromString(APP_VERSION);
185
186 if (!fileVersion.isNull())
187 {
188 if (appVersion < fileVersion)
189 {
190 qWarning() << "You are opening a newer project file in an older version of Pencil2D!";
191 }
192 }
193 }
194 else
195 {
196 Q_ASSERT(false);
197 }
198 }
199 return ok;
200}
201
202bool FileManager::loadObjectOldWay(Object* object, const QDomElement& root)
203{
204 return object->loadXML(root, [this] { progressForward(); });
205}
206
207bool FileManager::isArchiveFormat(const QString& fileName) const
208{
209 if (QFileInfo(fileName).suffix().compare(PFF_BIG_LETTER_EXTENSION, Qt::CaseInsensitive) != 0) {
210 return false;
211 }
212 return true;
213}
214
215Status FileManager::save(const Object* object, const QString& sFileName)
216{
217 DebugDetails dd;
218 dd << __FUNCTION__;
219 dd << ("sFileName = " + sFileName);
220
221 if (object == nullptr)
222 {
223 dd << "Object parameter is null";
224 return Status(Status::INVALID_ARGUMENT, dd);
225 }
226 if (sFileName.isEmpty()) {
227 dd << "File name is empty";
228 return Status(Status::INVALID_ARGUMENT, dd,
229 tr("Invalid Save Path"),
230 tr("The path is empty."));
231 }
232
233 const int totalCount = object->totalKeyFrameCount();
234 mMaxProgressValue = totalCount + 5;
235 emit progressRangeChanged(mMaxProgressValue);
236
237 progressForward();
238
239 QFileInfo fileInfo(sFileName);
240 if (fileInfo.isDir())
241 {
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()));
246 }
247 QFileInfo parentDirInfo(fileInfo.dir().absolutePath());
248 if (!parentDirInfo.exists())
249 {
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()));
254 }
255 if ((fileInfo.exists() && !fileInfo.isWritable()) || !parentDirInfo.isWritable())
256 {
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()));
261 }
262
263 QString sTempWorkingFolder;
264 QString sMainXMLFile;
265 QString sDataFolder;
266
267 bool isArchive = isArchiveFormat(sFileName);
268 if (!isArchive)
269 {
270 dd << "Old Pencil2D File Format (*.pcl) !";
271
272 sMainXMLFile = sFileName;
273 sDataFolder = sMainXMLFile + "." + PFF_OLD_DATA_DIR;
274 }
275 else
276 {
277 dd << "New zipped Pencil2D File Format (*.pclx) !";
278 dd.collect(MiniZ::sanityCheck(sFileName).details());
279
280 sTempWorkingFolder = object->workingDir();
281 Q_ASSERT(QDir(sTempWorkingFolder).exists());
282 dd << QString("TempWorkingFolder = ").append(sTempWorkingFolder);
283
284 sMainXMLFile = QDir(sTempWorkingFolder).filePath(PFF_XML_FILE_NAME);
285 sDataFolder = QDir(sTempWorkingFolder).filePath(PFF_OLD_DATA_DIR);
286 }
287
288 QFileInfo dataInfo(sDataFolder);
289 if (!dataInfo.exists())
290 {
291 QDir dir(sDataFolder); // the directory where all key frames will be saved
292
293 if (!dir.mkpath(sDataFolder))
294 {
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));
299 }
300 }
301 if (!dataInfo.isDir())
302 {
303 dd << QString("dataInfo.absoluteFilePath() = ").append(dataInfo.absoluteFilePath());
304 return Status(Status::FAIL,
305 dd,
306 tr("Cannot Create Data Directory"),
307 tr("\"%1\" is a file. Please delete the file and try again.").arg(dataInfo.absoluteFilePath()));
308 }
309
310 QStringList filesToZip; // A files list in the working folder needs to be zipped
311 Status stKeyFrames = writeKeyFrameFiles(object, sDataFolder, filesToZip);
312 dd.collect(stKeyFrames.details());
313
314 Status stMainXml = writeMainXml(object, sMainXMLFile, filesToZip);
315 dd.collect(stMainXml.details());
316
317 Status stPalette = writePalette(object, sDataFolder, filesToZip);
318 dd.collect(stPalette.details());
319
320 const bool saveOk = stKeyFrames.ok() && stMainXml.ok() && stPalette.ok();
321
322 progressForward();
323
324 if (isArchive)
325 {
326 QString sBackupFile = backupPreviousFile(sFileName);
327
328 if (!saveOk) {
329 return Status(Status::FAIL, dd,
330 tr("Internal Error"),
331 tr("An internal error occurred. Your file may not be saved successfully."));
332 }
333
334 dd << "Miniz";
335 Status stMiniz = MiniZ::compressFolder(sFileName, sTempWorkingFolder, filesToZip, "application/x-pencil2d-pclx");
336 if (!stMiniz.ok())
337 {
338 dd.collect(stMiniz.details());
339 return Status(Status::ERROR_MINIZ_FAIL, dd,
340 tr("Miniz Error"),
341 tr("An internal error occurred. Your file may not be saved successfully."));
342 }
343 dd << "Zip file saved successfully";
344 Q_ASSERT(stMiniz.ok());
345
346 if (saveOk)
347 deleteBackupFile(sBackupFile);
348 }
349
350 progressForward();
351
352 if (!saveOk)
353 {
354 return Status(Status::FAIL, dd,
355 tr("Internal Error"),
356 tr("An internal error occurred. Your file may not be saved successfully."));
357 }
358
359 return Status::OK;
360}
361
362Status FileManager::writeToWorkingFolder(const Object* object)
363{
364 DebugDetails dd;
365
366 QStringList filesWritten;
367
368 const QString dataFolder = object->dataDir();
369 const QString mainXml = object->mainXMLFile();
370
371 Status stKeyFrames = writeKeyFrameFiles(object, dataFolder, filesWritten);
372 dd.collect(stKeyFrames.details());
373
374 Status stMainXml = writeMainXml(object, mainXml, filesWritten);
375 dd.collect(stMainXml.details());
376
377 Status stPalette = writePalette(object, dataFolder, filesWritten);
378 dd.collect(stPalette.details());
379
380 const bool saveOk = stKeyFrames.ok() && stMainXml.ok() && stPalette.ok();
381 const auto errorCode = (saveOk) ? Status::OK : Status::FAIL;
382 return Status(errorCode, dd);
383}
384
385ObjectData FileManager::loadProjectData(const QDomElement& docElem)
386{
387 ObjectData data;
388 if (docElem.isNull())
389 {
390 return data;
391 }
392
393 QDomNode tag = docElem.firstChild();
394
395 while (!tag.isNull())
396 {
397 QDomElement element = tag.toElement(); // try to convert the node to an element.
398 if (element.isNull())
399 {
400 continue;
401 }
402
403 extractProjectData(element, data);
404
405 tag = tag.nextSibling();
406 }
407 return data;
408}
409
410QDomElement FileManager::saveProjectData(const ObjectData* data, QDomDocument& xmlDoc)
411{
412 QDomElement rootTag = xmlDoc.createElement("projectdata");
413
414 // Current Frame
415 QDomElement currentFrameTag = xmlDoc.createElement("currentFrame");
416 currentFrameTag.setAttribute("value", data->getCurrentFrame());
417 rootTag.appendChild(currentFrameTag);
418
419 // Current Color
420 QDomElement currentColorTag = xmlDoc.createElement("currentColor");
421 QColor color = data->getCurrentColor();
422 currentColorTag.setAttribute("r", color.red());
423 currentColorTag.setAttribute("g", color.green());
424 currentColorTag.setAttribute("b", color.blue());
425 currentColorTag.setAttribute("a", color.alpha());
426 rootTag.appendChild(currentColorTag);
427
428 // Current Layer
429 QDomElement currentLayerTag = xmlDoc.createElement("currentLayer");
430 currentLayerTag.setAttribute("value", data->getCurrentLayer());
431 rootTag.appendChild(currentLayerTag);
432
433 // Current View
434 QDomElement currentViewTag = xmlDoc.createElement("currentView");
435 QTransform view = data->getCurrentView();
436 currentViewTag.setAttribute("m11", view.m11());
437 currentViewTag.setAttribute("m12", view.m12());
438 currentViewTag.setAttribute("m21", view.m21());
439 currentViewTag.setAttribute("m22", view.m22());
440 currentViewTag.setAttribute("dx", view.dx());
441 currentViewTag.setAttribute("dy", view.dy());
442 rootTag.appendChild(currentViewTag);
443
444 // Fps
445 QDomElement fpsTag = xmlDoc.createElement("fps");
446 fpsTag.setAttribute("value", data->getFrameRate());
447 rootTag.appendChild(fpsTag);
448
449 // Current Layer
450 QDomElement tagIsLoop = xmlDoc.createElement("isLoop");
451 tagIsLoop.setAttribute("value", data->isLooping() ? "true" : "false");
452 rootTag.appendChild(tagIsLoop);
453
454 QDomElement tagRangedPlayback = xmlDoc.createElement("isRangedPlayback");
455 tagRangedPlayback.setAttribute("value", data->isRangedPlayback() ? "true" : "false");
456 rootTag.appendChild(tagRangedPlayback);
457
458 QDomElement tagMarkInFrame = xmlDoc.createElement("markInFrame");
459 tagMarkInFrame.setAttribute("value", data->getMarkInFrameNumber());
460 rootTag.appendChild(tagMarkInFrame);
461
462 QDomElement tagMarkOutFrame = xmlDoc.createElement("markOutFrame");
463 tagMarkOutFrame.setAttribute("value", data->getMarkOutFrameNumber());
464 rootTag.appendChild(tagMarkOutFrame);
465
466 return rootTag;
467}
468
469void FileManager::extractProjectData(const QDomElement& element, ObjectData& data)
470{
471 QString strName = element.tagName();
472 if (strName == "currentFrame")
473 {
474 data.setCurrentFrame(element.attribute("value").toInt());
475 }
476 else if (strName == "currentColor")
477 {
478 int r = element.attribute("r", "255").toInt();
479 int g = element.attribute("g", "255").toInt();
480 int b = element.attribute("b", "255").toInt();
481 int a = element.attribute("a", "255").toInt();
482
483 data.setCurrentColor(QColor(r, g, b, a));
484 }
485 else if (strName == "currentLayer")
486 {
487 data.setCurrentLayer(element.attribute("value", "0").toInt());
488 }
489 else if (strName == "currentView")
490 {
491 double m11 = element.attribute("m11", "1").toDouble();
492 double m12 = element.attribute("m12", "0").toDouble();
493 double m21 = element.attribute("m21", "0").toDouble();
494 double m22 = element.attribute("m22", "1").toDouble();
495 double dx = element.attribute("dx", "0").toDouble();
496 double dy = element.attribute("dy", "0").toDouble();
497
498 data.setCurrentView(QTransform(m11, m12, m21, m22, dx, dy));
499 }
500 else if (strName == "fps" || strName == "currentFps")
501 {
502 data.setFrameRate(element.attribute("value", "12").toInt());
503 }
504 else if (strName == "isLoop")
505 {
506 data.setLooping(element.attribute("value", "false") == "true");
507 }
508 else if (strName == "isRangedPlayback")
509 {
510 data.setRangedPlayback((element.attribute("value", "false") == "true"));
511 }
512 else if (strName == "markInFrame")
513 {
514 data.setMarkInFrameNumber(element.attribute("value", "0").toInt());
515 }
516 else if (strName == "markOutFrame")
517 {
518 data.setMarkOutFrameNumber(element.attribute("value", "15").toInt());
519 }
520}
521
522void FileManager::handleOpenProjectError(Status::ErrorCode error, const DebugDetails& dd)
523{
524 QString title = tr("Could not open file");
525 QString errorDesc;
526 QString contactLinks = "<ul>"
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>"
530 "</ul>";
531
532 if (error == Status::FILE_NOT_FOUND)
533 {
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.");
536 }
537 else if (error == Status::ERROR_FILE_CANNOT_OPEN)
538 {
539 errorDesc = tr("No permission to read the file. "
540 "Please check you have read permissions for this file and try again.");
541 }
542 else
543 {
544 // other cases
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:");
551 }
552
553 mError = Status(error, dd, title, errorDesc + contactLinks);
554 removePFFTmpDirectory(mstrLastTempFolder);
555}
556
557int FileManager::countExistingBackups(const QString& fileName) const
558{
559 QFileInfo fileInfo(fileName);
560 QDir directory(fileInfo.absoluteDir());
561 const QString& baseName = fileInfo.completeBaseName();
562
563 int backupCount = 0;
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)) {
567 backupCount++;
568 }
569 }
570
571 return backupCount;
572}
573
574QString FileManager::backupPreviousFile(const QString& fileName)
575{
576 if (!QFile::exists(fileName))
577 return "";
578
579 QFileInfo fileInfo(fileName);
580 QString baseName = fileInfo.completeBaseName();
581
582 int backupCount = countExistingBackups(fileName) + 1; // start index 1
583 QString countStr = QString::number(backupCount);
584
585 QString sBackupFile = baseName + "." + PFF_BACKUP_IDENTIFIER + countStr + "." + fileInfo.suffix();
586 QString sBackupFileFullPath = QDir(fileInfo.absolutePath()).filePath(sBackupFile);
587
588 bool ok = QFile::copy(fileInfo.absoluteFilePath(), sBackupFileFullPath);
589 if (!ok)
590 {
591 FILEMANAGER_LOG("Cannot backup the previous file");
592 return "";
593 }
594 return sBackupFileFullPath;
595}
596
597void FileManager::deleteBackupFile(const QString& fileName)
598{
599 if (QFile::exists(fileName))
600 {
601 QFile::remove(fileName);
602 }
603}
604
605void FileManager::progressForward()
606{
607 mCurrentProgress++;
608 emit progressChanged(mCurrentProgress);
609}
610
611bool FileManager::loadPalette(Object* obj)
612{
613 FILEMANAGER_LOG("Load Palette..");
614
615 QString paletteFilePath = QDir(obj->dataDir()).filePath(PFF_PALETTE_FILE);
616 if (!obj->importPalette(paletteFilePath))
617 {
618 obj->loadDefaultPalette();
619 }
620 return true;
621}
622
623Status FileManager::writeKeyFrameFiles(const Object* object, const QString& dataFolder, QStringList& filesFlushed)
624{
625 DebugDetails dd;
626
627 const int numLayers = object->getLayerCount();
628 dd << QString("Total %1 layers").arg(numLayers);
629
630 for (int i = 0; i < numLayers; ++i)
631 {
632 Layer* layer = object->getLayer(i);
633 layer->presave(dataFolder);
634 }
635
636 bool saveLayersOK = true;
637 for (int i = 0; i < numLayers; ++i)
638 {
639 Layer* layer = object->getLayer(i);
640
641 dd << QString("Layer[%1] = [id=%2, type=%3, name=%4]").arg(i).arg(layer->id()).arg(layer->type()).arg(layer->name());
642
643 Status st = layer->save(dataFolder, filesFlushed, [this] { progressForward(); });
644 if (!st.ok())
645 {
646 saveLayersOK = false;
647 dd.collect(st.details());
648 dd << QString(" !! Failed to save Layer[%1] %2").arg(i).arg(layer->name());
649 }
650 }
651 dd << "All Layers saved";
652
653 progressForward();
654
655 auto errorCode = (saveLayersOK) ? Status::OK : Status::FAIL;
656 return Status(errorCode, dd);
657}
658
659Status FileManager::writeMainXml(const Object* object, const QString& mainXmlPath, QStringList& filesWritten)
660{
661 DebugDetails dd;
662
663 QFile file(mainXmlPath);
664 if (!file.open(QFile::WriteOnly | QFile::Text))
665 {
666 dd << "Failed to open Main XML" << mainXmlPath;
667 return Status(Status::ERROR_FILE_CANNOT_OPEN, dd);
668 }
669
670 QDomDocument xmlDoc("PencilDocument");
671 QDomElement root = xmlDoc.createElement("document");
672 QDomProcessingInstruction encoding = xmlDoc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
673 xmlDoc.appendChild(encoding);
674 xmlDoc.appendChild(root);
675
676 progressForward();
677
678 // save editor information
679 QDomElement projDataXml = saveProjectData(object->data(), xmlDoc);
680 root.appendChild(projDataXml);
681
682 // save object
683 QDomElement objectElement = object->saveXML(xmlDoc);
684 root.appendChild(objectElement);
685
686 // save Pencil2D version
687 QDomElement versionElem = xmlDoc.createElement("version");
688 versionElem.appendChild(xmlDoc.createTextNode(QString(APP_VERSION)));
689 root.appendChild(versionElem);
690
691 dd << "Writing main xml file...";
692
693 const int indentSize = 2;
694
695 QTextStream out(&file);
696 xmlDoc.save(out, indentSize);
697 out.flush();
698 file.close();
699
700 dd << "Done writing main xml file: " << mainXmlPath;
701
702 filesWritten.append(mainXmlPath);
703 return Status(Status::OK, dd);
704}
705
706Status FileManager::writePalette(const Object* object, const QString& dataFolder, QStringList& filesWritten)
707{
708 const QString paletteFile = object->savePalette(dataFolder);
709 if (paletteFile.isEmpty())
710 {
711 DebugDetails dd;
712 dd << "Failed to save palette";
713 return Status(Status::FAIL, dd);
714 }
715 filesWritten.append(paletteFile);
716 return Status::OK;
717}
718
719Status FileManager::unzip(const QString& strZipFile, const QString& strUnzipTarget)
720{
721 // removes the previous directory first - better approach
722 removePFFTmpDirectory(strUnzipTarget);
723
724 Status s = MiniZ::uncompressFolder(strZipFile, strUnzipTarget);
725 Q_ASSERT(s.ok());
726
727 mstrLastTempFolder = strUnzipTarget;
728 return s;
729}
730
731QList<ColorRef> FileManager::loadPaletteFile(QString strFilename)
732{
733 QFileInfo fileInfo(strFilename);
734 if (!fileInfo.exists())
735 {
736 return QList<ColorRef>();
737 }
738
739 // TODO: Load Palette.
740 return QList<ColorRef>();
741}
742
743Status FileManager::verifyObject(Object* obj)
744{
745 // check current layer.
746 int curLayer = obj->data()->getCurrentLayer();
747 int maxLayer = obj->getLayerCount();
748 if (curLayer >= maxLayer)
749 {
750 obj->data()->setCurrentLayer(maxLayer - 1);
751 }
752
753 // Must have at least 1 camera layer
754 std::vector<LayerCamera*> camLayers = obj->getLayersByType<LayerCamera>();
755 if (camLayers.empty())
756 {
757 obj->addNewCameraLayer();
758 }
759 return Status::OK;
760}
761
762QStringList FileManager::searchForUnsavedProjects()
763{
764 QDir pencil2DTempDir = QDir::temp();
765 bool folderExists = pencil2DTempDir.cd("Pencil2D");
766 if (!folderExists)
767 {
768 return QStringList();
769 }
770
771 const QStringList nameFilter("*_" PFF_TMP_DECOMPRESS_EXT "_*"); // match name pattern like "Default_Y2xD_0a4e44e9"
772 QStringList entries = pencil2DTempDir.entryList(nameFilter, QDir::Dirs | QDir::Readable);
773
774 QStringList recoverables;
775 for (const QString& path : entries)
776 {
777 QString fullPath = pencil2DTempDir.filePath(path);
778 if (isProjectRecoverable(fullPath))
779 {
780 qDebug() << "Found debris at" << fullPath;
781 recoverables.append(fullPath);
782 }
783 }
784 return recoverables;
785}
786
787bool FileManager::isProjectRecoverable(const QString& projectFolder)
788{
789 QDir dir(projectFolder);
790 if (!dir.exists()) { return false; }
791
792 // There must be a subfolder called "data"
793 if (!dir.exists("data")) { return false; }
794
795 bool ok = dir.cd("data");
796 Q_ASSERT(ok);
797
798 QStringList nameFiler;
799 nameFiler << "*.png" << "*.vec" << "*.xml";
800 QStringList entries = dir.entryList(nameFiler, QDir::Files);
801
802 return (entries.size() > 0);
803}
804
805Object* FileManager::recoverUnsavedProject(QString intermeidatePath)
806{
807 qDebug() << "TODO: recover project" << intermeidatePath;
808
809 QDir projectDir(intermeidatePath);
810 const QString mainXMLPath = projectDir.filePath(PFF_XML_FILE_NAME);
811 const QString dataFolder = projectDir.filePath(PFF_DATA_DIR);
812
813 std::unique_ptr<Object> object(new Object);
814 object->setWorkingDir(intermeidatePath);
815 object->setMainXMLFile(mainXMLPath);
816 object->setDataDir(dataFolder);
817
818 Status st = recoverObject(object.get());
819 if (!st.ok())
820 {
821 mError = st;
822 return nullptr;
823 }
824 // Transfer ownership to the caller
825 return object.release();
826}
827
828Status FileManager::recoverObject(Object* object)
829{
830 // Check whether the main.xml is fine, if not we should make a valid one.
831 bool mainXmlOK = true;
832
833 QFile file(object->mainXMLFile());
834 mainXmlOK &= file.exists();
835 mainXmlOK &= file.open(QFile::ReadOnly);
836 file.close();
837
838 QDomDocument xmlDoc;
839 mainXmlOK &= !!xmlDoc.setContent(&file);
840
841 QDomDocumentType type = xmlDoc.doctype();
842 mainXmlOK &= (type.name() == "PencilDocument" || type.name() == "MyObject");
843
844 QDomElement root = xmlDoc.documentElement();
845 mainXmlOK &= (!root.isNull());
846
847 QDomElement objectTag = root.firstChildElement("object");
848 mainXmlOK &= (!objectTag.isNull());
849
850 if (mainXmlOK == false)
851 {
852 // the main.xml is broken, try to rebuild one
853 rebuildMainXML(object);
854
855 // Load the newly built main.xml
856 QFile file(object->mainXMLFile());
857 file.open(QFile::ReadOnly);
858 xmlDoc.setContent(&file);
859 root = xmlDoc.documentElement();
860 objectTag = root.firstChildElement("object");
861 }
862 loadPalette(object);
863
864 bool ok = loadObject(object, root);
865 verifyObject(object);
866
867 return ok ? Status::OK : Status::FAIL;
868}
869
871Status FileManager::rebuildMainXML(Object* object)
872{
873 QDir dataDir(object->dataDir());
874
875 QStringList nameFiler;
876 nameFiler << "*.png" << "*.vec";
877 const QStringList entries = dataDir.entryList(nameFiler, QDir::Files | QDir::Readable, QDir::Name);
878
879 QMap<int, QStringList> keyFrameGroups;
880
881 // grouping keyframe files by layers
882 for (const QString& s : entries)
883 {
884 int layerIndex = layerIndexFromFilename(s);
885 if (layerIndex > 0)
886 {
887 keyFrameGroups[layerIndex].append(s);
888 }
889 }
890
891 // build the new main XML file
892 const QString mainXMLPath = object->mainXMLFile();
893 QFile file(mainXMLPath);
894 if (!file.open(QFile::WriteOnly | QFile::Text))
895 {
896 return Status::ERROR_FILE_CANNOT_OPEN;
897 }
898
899 QDomDocument xmlDoc("PencilDocument");
900 QDomElement root = xmlDoc.createElement("document");
901 QDomProcessingInstruction encoding = xmlDoc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
902 xmlDoc.appendChild(encoding);
903 xmlDoc.appendChild(root);
904
905 // save editor information
906 QDomElement projDataXml = saveProjectData(object->data(), xmlDoc);
907 root.appendChild(projDataXml);
908
909 // save object
910 QDomElement elemObject = xmlDoc.createElement("object");
911 root.appendChild(elemObject);
912
913 for (const int layerIndex : keyFrameGroups.keys())
914 {
915 const QStringList& frames = keyFrameGroups.value(layerIndex);
916 Status st = rebuildLayerXmlTag(xmlDoc, elemObject, layerIndex, frames);
917 }
918
919 QTextStream fout(&file);
920 xmlDoc.save(fout, 2);
921 fout.flush();
922 file.close();
923
924 return Status::OK;
925}
934Status FileManager::rebuildLayerXmlTag(QDomDocument& doc,
935 QDomElement& elemObject,
936 const int layerIndex,
937 const QStringList& frames)
938{
939 Q_ASSERT(frames.length() > 0);
940
941 Layer::LAYER_TYPE type = frames[0].endsWith(".png") ? Layer::BITMAP : Layer::VECTOR;
942
943 QDomElement elemLayer = doc.createElement("layer");
944 elemLayer.setAttribute("id", layerIndex + 1); // starts from 1, not 0.
945 elemLayer.setAttribute("name", recoverLayerName(type, layerIndex));
946 elemLayer.setAttribute("visibility", true);
947 elemLayer.setAttribute("type", type);
948 elemObject.appendChild(elemLayer);
949
950 for (const QString& s : frames)
951 {
952 const int framePos = framePosFromFilename(s);
953 if (framePos < 0) { continue; }
954
955 QDomElement elemFrame = doc.createElement("image");
956 elemFrame.setAttribute("frame", framePos);
957 elemFrame.setAttribute("src", s);
958
959 if (type == Layer::BITMAP)
960 {
961 // Since we have no way to know the original img position
962 // Put it at the top left corner of the default camera
963 elemFrame.setAttribute("topLeftX", -800);
964 elemFrame.setAttribute("topLeftY", -600);
965 }
966 elemLayer.appendChild(elemFrame);
967 }
968 return Status::OK;
969}
970
971QString FileManager::recoverLayerName(Layer::LAYER_TYPE type, int index)
972{
973 switch (type)
974 {
975 case Layer::BITMAP:
976 return tr("Bitmap Layer %1").arg(index);
977 case Layer::VECTOR:
978 return tr("Vector Layer %1").arg(index);
979 case Layer::SOUND:
980 return tr("Sound Layer %1").arg(index);
981 default:
982 Q_ASSERT(false);
983 }
984 return "";
985}
986
987int FileManager::layerIndexFromFilename(const QString& filename)
988{
989 const QStringList tokens = filename.split("."); // e.g., 001.019.png or 012.132.vec
990 if (tokens.length() >= 3) // a correct file name must have 3 tokens
991 {
992 return tokens[0].toInt();
993 }
994 return -1;
995}
996
997int FileManager::framePosFromFilename(const QString& filename)
998{
999 const QStringList tokens = filename.split("."); // e.g., 001.019.png or 012.132.vec
1000 if (tokens.length() >= 3) // a correct file name must have 3 tokens
1001 {
1002 return tokens[1].toInt();
1003 }
1004 return -1;
1005}
DebugDetails
Definition: pencilerror.h:25
FileManager::rebuildMainXML
Status rebuildMainXML(Object *object)
Create a new main.xml based on the png/vec filenames left in the data folder.
Definition: filemanager.cpp:871
FileManager::rebuildLayerXmlTag
Status rebuildLayerXmlTag(QDomDocument &doc, QDomElement &elemObject, const int layerIndex, const QStringList &frames)
Rebuild a layer xml tag.
Definition: filemanager.cpp:934
LayerCamera
Definition: layercamera.h:30
Layer
Definition: layer.h:37
ObjectData
Definition: objectdata.h:27
Object
Definition: object.h:42
Status
Definition: pencilerror.h:40
QColor
QColor::alpha
int alpha() const const
QColor::blue
int blue() const const
QColor::green
int green() const const
QColor::red
int red() const const
QDir
QDir::Files
Files
QDir::Name
Name
QDir::cd
bool cd(const QString &dirName)
QDir::entryList
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
QDir::filePath
QString filePath(const QString &fileName) const const
QDir::temp
QDir temp()
QDomDocument
QDomDocument::createElement
QDomElement createElement(const QString &tagName)
QDomDocument::createProcessingInstruction
QDomProcessingInstruction createProcessingInstruction(const QString &target, const QString &data)
QDomDocument::createTextNode
QDomText createTextNode(const QString &value)
QDomDocument::doctype
QDomDocumentType doctype() const const
QDomDocument::documentElement
QDomElement documentElement() const const
QDomDocument::setContent
bool setContent(const QByteArray &data, bool namespaceProcessing, QString *errorMsg, int *errorLine, int *errorColumn)
QDomDocumentType
QDomDocumentType::name
QString name() const const
QDomElement
QDomElement::attribute
QString attribute(const QString &name, const QString &defValue) const const
QDomElement::setAttribute
void setAttribute(const QString &name, const QString &value)
QDomElement::tagName
QString tagName() const const
QDomElement::text
QString text() const const
QDomNode
QDomNode::appendChild
QDomNode appendChild(const QDomNode &newChild)
QDomNode::firstChild
QDomNode firstChild() const const
QDomNode::firstChildElement
QDomElement firstChildElement(const QString &tagName) const const
QDomNode::isNull
bool isNull() const const
QDomNode::nextSibling
QDomNode nextSibling() const const
QDomNode::save
void save(QTextStream &stream, int indent, QDomNode::EncodingPolicy encodingPolicy) const const
QDomNode::toElement
QDomElement toElement() const const
QDomProcessingInstruction
QFile
QFile::copy
bool copy(const QString &newName)
QFile::exists
bool exists() const const
QFile::open
virtual bool open(QIODevice::OpenMode mode) override
QFile::remove
bool remove()
QFileDevice::close
virtual void close() override
QFileInfo
QIODevice::ReadOnly
ReadOnly
QList
QList::append
void append(const T &value)
QList::endsWith
bool endsWith(const T &value) const const
QList::length
int length() const const
QList::size
int size() const const
QMap
QMap::keys
QList< Key > keys() const const
QMap::value
const T value(const Key &key, const T &defaultValue) const const
QObject
QObject::tr
QString tr(const char *sourceText, const char *disambiguation, int n)
QString::split
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString
QString::append
QString & append(QChar ch)
QString::arg
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString::compare
int compare(const QString &other, Qt::CaseSensitivity cs) const const
QString::contains
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString::isEmpty
bool isEmpty() const const
QString::number
QString number(int n, int base)
QString::toDouble
double toDouble(bool *ok) const const
QString::toInt
int toInt(bool *ok, int base) const const
QStringList
Qt::CaseInsensitive
CaseInsensitive
QTextStream
QTextStream::flush
void flush()
QTransform
QTransform::dx
qreal dx() const const
QTransform::dy
qreal dy() const const
QTransform::m11
qreal m11() const const
QTransform::m12
qreal m12() const const
QTransform::m21
qreal m21() const const
QTransform::m22
qreal m22() const const
QVersionNumber
QVersionNumber::fromString
QVersionNumber fromString(const QString &string, int *suffixIndex)
QVersionNumber::isNull
bool isNull() const const
Generated on Sat Nov 25 2023 13:00:01 for Pencil2D by doxygen 1.9.6 based on revision 18c5494f61f228a0f7b8820420627d4ac76231a8