Pencil2D Animation
Download Community News Docs Contribute
  • Overview
  • Articles
  • Code
  •  
  • Class List
  • Class Index
  • Class Hierarchy
  • Class Members
  • 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 <QDebug>
22#include <QDir>
23#include <QVersionNumber>
24#include "qminiz.h"
25#include "fileformat.h"
26#include "object.h"
27#include "layercamera.h"
28#include "util.h"
29
30FileManager::FileManager(QObject* parent) : QObject(parent)
31{
32 srand(static_cast<uint>(time(nullptr)));
33}
34
35Object* FileManager::load(const QString& sFileName)
36{
37 DebugDetails dd;
38 dd << "\n[Project LOAD diagnostics]\n";
39 dd << QString("File name: ").append(sFileName);
40 if (!QFile::exists(sFileName))
41 {
42 handleOpenProjectError(Status::FILE_NOT_FOUND, dd);
43 return nullptr;
44 }
45
46 progressForward();
47
48 std::unique_ptr<Object> obj(new Object);
49 obj->setFilePath(sFileName);
50 obj->createWorkingDir();
51
52 QString strMainXMLFile = QDir(obj->workingDir()).filePath(PFF_XML_FILE_NAME);
53 QString strDataFolder = QDir(obj->workingDir()).filePath(PFF_DATA_DIR);
54
55 // Test file format: new zipped .pclx or old .pcl?
56 bool isArchive = isArchiveFormat(sFileName);
57
58 QString fileFormat = "Project format: %1";
59 if (!isArchive)
60 {
61 dd << fileFormat.arg(".pcl");
62
63 if (!QFile::copy(sFileName, strMainXMLFile))
64 {
65 dd << "Failed to copy main xml file";
66 }
67 Status st = copyDir(sFileName + "." + PFF_OLD_DATA_DIR, strDataFolder);
68 if (!st.ok())
69 {
70 dd.collect(st.details());
71 }
72 }
73 else
74 {
75 QString workingDirPath = obj->workingDir();
76
77 dd << QString("Working dir: %1").arg(workingDirPath);
78 dd << fileFormat.arg(".pclx");
79
80 Status sanityCheck = MiniZ::sanityCheck(sFileName);
81
82 // Let's check if we can read the file before we try to unzip.
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);
87 return nullptr;
88 } else {
89 Status unzipStatus = unzip(sFileName, workingDirPath);
90 dd.collect(unzipStatus.details());
91
92 if(unzipStatus.ok()) {
93 dd << QString("Unzipped at: %1 ").arg(workingDirPath);
94 } else {
95 dd << QString("Error: Unzipping failed: %1 ").arg(workingDirPath);
96 handleOpenProjectError(Status::ERROR_INVALID_XML_FILE, dd);
97 return nullptr;
98 }
99 }
100 }
101
102 obj->setDataDir(strDataFolder);
103 obj->setMainXMLFile(strMainXMLFile);
104
105 int totalFileCount = QDir(strDataFolder).entryList(QDir::Files).size();
106 mMaxProgressValue = totalFileCount;
107 emit progressRangeChanged(mMaxProgressValue);
108
109 QFile file(strMainXMLFile);
110 if (!file.exists())
111 {
112 dd << "Error: No main XML exists!";
113 handleOpenProjectError(Status::ERROR_INVALID_XML_FILE, dd);
114 return nullptr;
115 }
116 if (!file.open(QFile::ReadOnly))
117 {
118 dd << "Error: Main XML file is read only!";
119 handleOpenProjectError(Status::ERROR_FILE_CANNOT_OPEN, dd);
120 return nullptr;
121 }
122
123 dd << "Main XML exists: Yes";
124
125 QDomDocument xmlDoc;
126 if (!xmlDoc.setContent(&file))
127 {
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);
131 return nullptr;
132 }
133
134 QDomDocumentType type = xmlDoc.doctype();
135 if (!(type.name() == "PencilDocument" || type.name() == "MyObject"))
136 {
137 FILEMANAGER_LOG("Invalid main XML doctype");
138 dd << QString("Error: Invalid main XML doctype: ").append(type.name());
139 handleOpenProjectError(Status::ERROR_INVALID_PENCIL_FILE, dd);
140 return nullptr;
141 }
142
143 QDomElement root = xmlDoc.documentElement();
144 if (root.isNull())
145 {
146 dd << "Error: Main XML root node is null";
147 handleOpenProjectError(Status::ERROR_INVALID_PENCIL_FILE, dd);
148 return nullptr;
149 }
150
151 loadPalette(obj.get());
152
153 bool ok = true;
154
155 if (root.tagName() == "document")
156 {
157 ok = loadObject(obj.get(), root);
158 }
159 else if (root.tagName() == "object" || root.tagName() == "MyOject") // old Pencil format (<=0.4.3)
160 {
161 ok = loadObjectOldWay(obj.get(), root);
162 }
163
164 if (!ok)
165 {
166 obj.reset();
167 dd << "Error: Issue occurred during object loading";
168 handleOpenProjectError(Status::ERROR_INVALID_PENCIL_FILE, dd);
169 return nullptr;
170 }
171
172 verifyObject(obj.get());
173
174 return obj.release();
175}
176
177bool FileManager::loadObject(Object* object, const QDomElement& root)
178{
179 QDomElement e = root.firstChildElement("object");
180 if (e.isNull())
181 return false;
182
183 bool ok = true;
184 for (QDomNode node = root.firstChild(); !node.isNull(); node = node.nextSibling())
185 {
186 QDomElement element = node.toElement(); // try to convert the node to an element.
187 if (element.isNull())
188 {
189 continue;
190 }
191
192 if (element.tagName() == "object")
193 {
194 ok = object->loadXML(element, [this]{ progressForward(); });
195 if (!ok) FILEMANAGER_LOG("Failed to Load object");
196
197 }
198 else if (element.tagName() == "editor" || element.tagName() == "projectdata")
199 {
200 object->setData(loadProjectData(element));
201 }
202 else if (element.tagName() == "version")
203 {
204 QVersionNumber fileVersion = QVersionNumber::fromString(element.text());
205 QVersionNumber appVersion = QVersionNumber::fromString(APP_VERSION);
206
207 if (!fileVersion.isNull())
208 {
209 if (appVersion < fileVersion)
210 {
211 qWarning() << "You are opening a newer project file in an older version of Pencil2D!";
212 }
213 }
214 }
215 else
216 {
217 Q_ASSERT(false);
218 }
219 }
220 return ok;
221}
222
223bool FileManager::loadObjectOldWay(Object* object, const QDomElement& root)
224{
225 return object->loadXML(root, [this] { progressForward(); });
226}
227
228bool FileManager::isArchiveFormat(const QString& fileName) const
229{
230 if (QFileInfo(fileName).suffix().compare(PFF_BIG_LETTER_EXTENSION, Qt::CaseInsensitive) != 0) {
231 return false;
232 }
233 return true;
234}
235
236Status FileManager::save(const Object* object, const QString& sFileName)
237{
238 DebugDetails dd;
239 dd << "\n[Project SAVE diagnostics]\n";
240 dd << ("file name:" + sFileName);
241
242 if (object == nullptr)
243 {
244 dd << "Error: Object parameter is null";
245 return Status(Status::INVALID_ARGUMENT, dd);
246 }
247 if (sFileName.isEmpty()) {
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."));
252 }
253
254 const int totalCount = object->totalKeyFrameCount();
255 mMaxProgressValue = totalCount + 5;
256 emit progressRangeChanged(mMaxProgressValue);
257
258 progressForward();
259
260 QFileInfo fileInfo(sFileName);
261 if (fileInfo.isDir())
262 {
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()));
267 }
268 QFileInfo parentDirInfo(fileInfo.dir().absolutePath());
269 if (!parentDirInfo.exists())
270 {
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()));
275 }
276 if ((fileInfo.exists() && !fileInfo.isWritable()) || !parentDirInfo.isWritable())
277 {
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()));
282 }
283
284 QString sTempWorkingFolder;
285 QString sMainXMLFile;
286 QString sDataFolder;
287
288 QString fileFormat = QString("Project format: %1");
289 bool isArchive = isArchiveFormat(sFileName);
290 if (!isArchive)
291 {
292 dd << fileFormat.arg(".pcl");
293
294 sMainXMLFile = sFileName;
295 sDataFolder = sMainXMLFile + "." + PFF_OLD_DATA_DIR;
296 }
297 else
298 {
299 sTempWorkingFolder = object->workingDir();
300
301 dd << QString("Working dir: %1").arg(sTempWorkingFolder);
302 dd << fileFormat.arg(".pclx");
303
304 Q_ASSERT(QDir(sTempWorkingFolder).exists());
305
306 sMainXMLFile = QDir(sTempWorkingFolder).filePath(PFF_XML_FILE_NAME);
307 sDataFolder = QDir(sTempWorkingFolder).filePath(PFF_OLD_DATA_DIR);
308 }
309
310 QFileInfo dataInfo(sDataFolder);
311 if (!dataInfo.exists())
312 {
313 QDir dir(sDataFolder); // the directory where all key frames will be saved
314
315 if (!dir.mkpath(sDataFolder))
316 {
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));
321 }
322 }
323 if (!dataInfo.isDir())
324 {
325 dd << QString("Error: Expected data to be a directory but found %1 instead").arg(dataInfo.absoluteFilePath());
326 return Status(Status::FAIL,
327 dd,
328 tr("Cannot Create Data Directory"),
329 tr("\"%1\" is a file. Please delete the file and try again.").arg(dataInfo.absoluteFilePath()));
330 }
331
332 QStringList filesToZip; // A files list in the working folder needs to be zipped
333 Status stKeyFrames = writeKeyFrameFiles(object, sDataFolder, filesToZip);
334 dd.collect(stKeyFrames.details());
335
336 Status stMainXml = writeMainXml(object, sMainXMLFile, filesToZip);
337 dd.collect(stMainXml.details());
338
339 Status stPalette = writePalette(object, sDataFolder, filesToZip);
340 dd.collect(stPalette.details());
341
342 const bool saveOk = stKeyFrames.ok() && stMainXml.ok() && stPalette.ok();
343
344 progressForward();
345
346 if (isArchive)
347 {
348 dd << "\n[Archiving diagnostics]\n";
349 QString sBackupFile = backupPreviousFile(sFileName);
350
351 if (!sBackupFile.isEmpty()) {
352 dd << QString("\nNote: A backup has been made here: %1").arg(sBackupFile);
353 }
354
355 if (!saveOk) {
356 return Status(Status::FAIL, dd,
357 tr("Internal Error"),
358 tr("An internal error occurred. The project could not be saved."));
359 }
360
361 dd << "Miniz: Zipping...";
362 Status stMiniz = MiniZ::compressFolder(sFileName, sTempWorkingFolder, filesToZip, "application/x-pencil2d-pclx");
363 if (!stMiniz.ok())
364 {
365 dd.collect(stMiniz.details());
366 dd << "\nError: Miniz failed to zip project";
367 return Status(Status::ERROR_MINIZ_FAIL, dd,
368 tr("Miniz Error"),
369 tr("An internal error occurred. The project may not have been saved successfully."));
370 }
371 dd << "Miniz: Zip file saved successfully";
372 Q_ASSERT(stMiniz.ok());
373
374 if (saveOk) {
375 dd << "Project saved successfully, deleting backup";
376 deleteBackupFile(sBackupFile);
377 }
378 }
379
380 progressForward();
381
382 if (!saveOk)
383 {
384 return Status(Status::FAIL, dd,
385 tr("Internal Error"),
386 tr("An internal error occurred. The project may not have been saved successfully."));
387 }
388
389 return Status::OK;
390}
391
392Status FileManager::writeToWorkingFolder(const Object* object)
393{
394 DebugDetails dd;
395
396 QStringList filesWritten;
397
398 const QString dataFolder = object->dataDir();
399 const QString mainXml = object->mainXMLFile();
400
401 Status stKeyFrames = writeKeyFrameFiles(object, dataFolder, filesWritten);
402 dd.collect(stKeyFrames.details());
403
404 Status stMainXml = writeMainXml(object, mainXml, filesWritten);
405 dd.collect(stMainXml.details());
406
407 Status stPalette = writePalette(object, dataFolder, filesWritten);
408 dd.collect(stPalette.details());
409
410 const bool saveOk = stKeyFrames.ok() && stMainXml.ok() && stPalette.ok();
411 const auto errorCode = (saveOk) ? Status::OK : Status::FAIL;
412 return Status(errorCode, dd);
413}
414
415ObjectData FileManager::loadProjectData(const QDomElement& docElem)
416{
417 ObjectData data;
418 if (docElem.isNull())
419 {
420 return data;
421 }
422
423 QDomNode tag = docElem.firstChild();
424
425 while (!tag.isNull())
426 {
427 QDomElement element = tag.toElement(); // try to convert the node to an element.
428 if (element.isNull())
429 {
430 continue;
431 }
432
433 extractProjectData(element, data);
434
435 tag = tag.nextSibling();
436 }
437 return data;
438}
439
440QDomElement FileManager::saveProjectData(const ObjectData* data, QDomDocument& xmlDoc)
441{
442 QDomElement rootTag = xmlDoc.createElement("projectdata");
443
444 // Current Frame
445 QDomElement currentFrameTag = xmlDoc.createElement("currentFrame");
446 currentFrameTag.setAttribute("value", data->getCurrentFrame());
447 rootTag.appendChild(currentFrameTag);
448
449 // Current Color
450 QDomElement currentColorTag = xmlDoc.createElement("currentColor");
451 QColor color = data->getCurrentColor();
452 currentColorTag.setAttribute("r", color.red());
453 currentColorTag.setAttribute("g", color.green());
454 currentColorTag.setAttribute("b", color.blue());
455 currentColorTag.setAttribute("a", color.alpha());
456 rootTag.appendChild(currentColorTag);
457
458 // Current Layer
459 QDomElement currentLayerTag = xmlDoc.createElement("currentLayer");
460 currentLayerTag.setAttribute("value", data->getCurrentLayer());
461 rootTag.appendChild(currentLayerTag);
462
463 // Current View
464 QDomElement currentViewTag = xmlDoc.createElement("currentView");
465 QTransform view = data->getCurrentView();
466 currentViewTag.setAttribute("m11", view.m11());
467 currentViewTag.setAttribute("m12", view.m12());
468 currentViewTag.setAttribute("m21", view.m21());
469 currentViewTag.setAttribute("m22", view.m22());
470 currentViewTag.setAttribute("dx", view.dx());
471 currentViewTag.setAttribute("dy", view.dy());
472 rootTag.appendChild(currentViewTag);
473
474 // Fps
475 QDomElement fpsTag = xmlDoc.createElement("fps");
476 fpsTag.setAttribute("value", data->getFrameRate());
477 rootTag.appendChild(fpsTag);
478
479 // Current Layer
480 QDomElement tagIsLoop = xmlDoc.createElement("isLoop");
481 tagIsLoop.setAttribute("value", data->isLooping() ? "true" : "false");
482 rootTag.appendChild(tagIsLoop);
483
484 QDomElement tagRangedPlayback = xmlDoc.createElement("isRangedPlayback");
485 tagRangedPlayback.setAttribute("value", data->isRangedPlayback() ? "true" : "false");
486 rootTag.appendChild(tagRangedPlayback);
487
488 QDomElement tagMarkInFrame = xmlDoc.createElement("markInFrame");
489 tagMarkInFrame.setAttribute("value", data->getMarkInFrameNumber());
490 rootTag.appendChild(tagMarkInFrame);
491
492 QDomElement tagMarkOutFrame = xmlDoc.createElement("markOutFrame");
493 tagMarkOutFrame.setAttribute("value", data->getMarkOutFrameNumber());
494 rootTag.appendChild(tagMarkOutFrame);
495
496 return rootTag;
497}
498
499void FileManager::extractProjectData(const QDomElement& element, ObjectData& data)
500{
501 QString strName = element.tagName();
502 if (strName == "currentFrame")
503 {
504 data.setCurrentFrame(element.attribute("value").toInt());
505 }
506 else if (strName == "currentColor")
507 {
508 int r = element.attribute("r", "255").toInt();
509 int g = element.attribute("g", "255").toInt();
510 int b = element.attribute("b", "255").toInt();
511 int a = element.attribute("a", "255").toInt();
512
513 data.setCurrentColor(QColor(r, g, b, a));
514 }
515 else if (strName == "currentLayer")
516 {
517 data.setCurrentLayer(element.attribute("value", "0").toInt());
518 }
519 else if (strName == "currentView")
520 {
521 double m11 = element.attribute("m11", "1").toDouble();
522 double m12 = element.attribute("m12", "0").toDouble();
523 double m21 = element.attribute("m21", "0").toDouble();
524 double m22 = element.attribute("m22", "1").toDouble();
525 double dx = element.attribute("dx", "0").toDouble();
526 double dy = element.attribute("dy", "0").toDouble();
527
528 data.setCurrentView(QTransform(m11, m12, m21, m22, dx, dy));
529 }
530 else if (strName == "fps" || strName == "currentFps")
531 {
532 data.setFrameRate(element.attribute("value", "12").toInt());
533 }
534 else if (strName == "isLoop")
535 {
536 data.setLooping(element.attribute("value", "false") == "true");
537 }
538 else if (strName == "isRangedPlayback")
539 {
540 data.setRangedPlayback((element.attribute("value", "false") == "true"));
541 }
542 else if (strName == "markInFrame")
543 {
544 data.setMarkInFrameNumber(element.attribute("value", "0").toInt());
545 }
546 else if (strName == "markOutFrame")
547 {
548 data.setMarkOutFrameNumber(element.attribute("value", "15").toInt());
549 }
550}
551
552void FileManager::handleOpenProjectError(Status::ErrorCode error, const DebugDetails& dd)
553{
554 QString title = tr("Could not open file");
555 QString errorDesc;
556 QString contactLinks = "<ul>"
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>"
560 "</ul>";
561
562 if (error == Status::FILE_NOT_FOUND)
563 {
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.");
566 }
567 else if (error == Status::ERROR_FILE_CANNOT_OPEN)
568 {
569 errorDesc = tr("No permission to read the file. "
570 "Please check you have read permissions for this file and try again.");
571 }
572 else
573 {
574 // other cases
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:");
581 }
582
583 mError = Status(error, dd, title, errorDesc + contactLinks);
584 removePFFTmpDirectory(mstrLastTempFolder);
585}
586
587int FileManager::countExistingBackups(const QString& fileName) const
588{
589 QFileInfo fileInfo(fileName);
590 QDir directory(fileInfo.absoluteDir());
591 const QString& baseFileName = fileInfo.completeBaseName();
592
593 int backupCount = 0;
594 for (const QFileInfo &dirFileInfo : directory.entryInfoList(QDir::Filter::Files)) {
595 QString searchFileAbsPath = dirFileInfo.absoluteFilePath();
596 QString searchFileName = dirFileInfo.baseName();
597
598 bool sameBaseName = baseFileName.compare(searchFileName) == 0;
599 if (sameBaseName && searchFileAbsPath.contains(PFF_BACKUP_IDENTIFIER)) {
600 backupCount++;
601 }
602 }
603
604 return backupCount;
605}
606
607QString FileManager::backupPreviousFile(const QString& fileName)
608{
609 if (!QFile::exists(fileName))
610 return "";
611
612 QFileInfo fileInfo(fileName);
613 QString baseName = fileInfo.completeBaseName();
614
615 int backupCount = countExistingBackups(fileName) + 1; // start index 1
616 QString countStr = QString::number(backupCount);
617
618 QString sBackupFile = baseName + "." + PFF_BACKUP_IDENTIFIER + countStr + "." + fileInfo.suffix();
619 QString sBackupFileFullPath = QDir(fileInfo.absolutePath()).filePath(sBackupFile);
620
621 bool ok = QFile::copy(fileInfo.absoluteFilePath(), sBackupFileFullPath);
622 if (!ok)
623 {
624 FILEMANAGER_LOG("Cannot backup the previous file");
625 return "";
626 }
627 return sBackupFileFullPath;
628}
629
630void FileManager::deleteBackupFile(const QString& fileName)
631{
632 if (QFile::exists(fileName))
633 {
634 QFile::remove(fileName);
635 }
636}
637
638void FileManager::progressForward()
639{
640 mCurrentProgress++;
641 emit progressChanged(mCurrentProgress);
642}
643
644bool FileManager::loadPalette(Object* obj)
645{
646 FILEMANAGER_LOG("Load Palette..");
647
648 QString paletteFilePath = validateDataPath(PFF_PALETTE_FILE, obj->dataDir());
649 if (paletteFilePath.isEmpty() || !obj->importPalette(paletteFilePath))
650 {
651 obj->loadDefaultPalette();
652 }
653 return true;
654}
655
656Status FileManager::writeKeyFrameFiles(const Object* object, const QString& dataFolder, QStringList& filesFlushed)
657{
658 DebugDetails dd;
659 dd << "\n[Keyframes WRITE diagnostics]\n";
660
661 const int numLayers = object->getLayerCount();
662 dd << QString("Total layer count: %1").arg(numLayers);
663
664 for (int i = 0; i < numLayers; ++i)
665 {
666 Layer* layer = object->getLayer(i);
667 layer->presave(dataFolder);
668 }
669
670 bool saveLayersOK = true;
671 for (int i = 0; i < numLayers; ++i)
672 {
673 Layer* layer = object->getLayer(i);
674
675 dd << QString("Layer[%1] = [id=%2, type=%3, name=%4]").arg(i).arg(layer->id()).arg(layer->type()).arg(layer->name());
676
677 Status st = layer->save(dataFolder, filesFlushed, [this] { progressForward(); });
678 if (!st.ok())
679 {
680 saveLayersOK = false;
681 dd.collect(st.details());
682 dd << QString("\nError: Failed to save Layer[%1] %2").arg(i).arg(layer->name());
683 }
684 }
685
686 progressForward();
687
688 auto errorCode = (saveLayersOK) ? Status::OK : Status::FAIL;
689
690 if (saveLayersOK) {
691 dd << "\nAll Layers saved";
692 } else {
693 dd << "\nError: Unable to save all layers";
694 }
695
696 return Status(errorCode, dd);
697}
698
699Status FileManager::writeMainXml(const Object* object, const QString& mainXmlPath, QStringList& filesWritten)
700{
701 DebugDetails dd;
702 dd << "\n[XML WRITE diagnostics]\n";
703
704 QFile file(mainXmlPath);
705 if (!file.open(QFile::WriteOnly | QFile::Text))
706 {
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);
709 }
710 ScopeGuard fileScopeGuard([&] {
711 file.close();
712 });
713
714 QDomDocument xmlDoc("PencilDocument");
715 QDomElement root = xmlDoc.createElement("document");
716 QDomProcessingInstruction encoding = xmlDoc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
717 xmlDoc.appendChild(encoding);
718 xmlDoc.appendChild(root);
719
720 progressForward();
721
722 // save editor information
723 QDomElement projDataXml = saveProjectData(object->data(), xmlDoc);
724 root.appendChild(projDataXml);
725
726 // save object
727 QDomElement objectElement = object->saveXML(xmlDoc);
728 root.appendChild(objectElement);
729
730 // save Pencil2D version
731 QDomElement versionElem = xmlDoc.createElement("version");
732 versionElem.appendChild(xmlDoc.createTextNode(QString(APP_VERSION)));
733 root.appendChild(versionElem);
734
735 dd << "Writing main xml file...";
736
737 const int indentSize = 2;
738
739 QTextStream out(&file);
740 xmlDoc.save(out, indentSize);
741 out.flush();
742
743 dd << "Done writing main xml file: " << mainXmlPath;
744
745 filesWritten.append(mainXmlPath);
746 return Status(Status::OK, dd);
747}
748
749Status FileManager::writePalette(const Object* object, const QString& dataFolder, QStringList& filesWritten)
750{
751 const QString paletteFile = object->savePalette(dataFolder);
752 if (paletteFile.isEmpty())
753 {
754 DebugDetails dd;
755 dd << "\nError: Failed to save palette";
756 return Status(Status::FAIL, dd);
757 }
758 filesWritten.append(paletteFile);
759 return Status::OK;
760}
761
775Status FileManager::copyDir(const QDir src, const QDir dst)
776{
777 if (!src.exists()) return Status::FILE_NOT_FOUND;
778
779 DebugDetails dd;
780 bool isOkay = true;
781
782 if (!dst.mkpath(dst.absolutePath()))
783 {
784 dd << ("Failed to create target directory: " + dst.absolutePath());
785 return Status(Status::FAIL, dd);
786 }
787
788 foreach (QString dirName, src.entryList(QDir::Dirs | QDir::NoDotAndDotDot))
789 {
790 Status st = copyDir(src.filePath(dirName), dst.filePath(dirName));
791 if (!st.ok())
792 {
793 isOkay = false;
794 dd.collect(st.details());
795 }
796 }
797
798 foreach (QString fileName, src.entryList(QDir::Files))
799 {
800 if (!QFile::copy(src.filePath(fileName), dst.filePath(fileName)))
801 {
802 isOkay = false;
803 dd << "Failed to copy file"
804 << ("Source path: " + src.filePath(fileName))
805 << ("Destination path: " + dst.filePath(fileName));
806 }
807 }
808
809 if (isOkay)
810 {
811 return Status::OK;
812 }
813 else
814 {
815 return Status(Status::FAIL, dd);
816 }
817}
818
819Status FileManager::unzip(const QString& strZipFile, const QString& strUnzipTarget)
820{
821 // removes the previous directory first - better approach
822 removePFFTmpDirectory(strUnzipTarget);
823
824 Status s = MiniZ::uncompressFolder(strZipFile, strUnzipTarget);
825 Q_ASSERT(s.ok());
826
827 mstrLastTempFolder = strUnzipTarget;
828 return s;
829}
830
831QList<ColorRef> FileManager::loadPaletteFile(QString strFilename)
832{
833 QFileInfo fileInfo(strFilename);
834 if (!fileInfo.exists())
835 {
836 return QList<ColorRef>();
837 }
838
839 // TODO: Load Palette.
840 return QList<ColorRef>();
841}
842
843Status FileManager::verifyObject(Object* obj)
844{
845 // check current layer.
846 int curLayer = obj->data()->getCurrentLayer();
847 int maxLayer = obj->getLayerCount();
848 if (curLayer >= maxLayer)
849 {
850 obj->data()->setCurrentLayer(maxLayer - 1);
851 }
852
853 // Must have at least 1 camera layer
854 std::vector<LayerCamera*> camLayers = obj->getLayersByType<LayerCamera>();
855 if (camLayers.empty())
856 {
857 obj->addNewCameraLayer();
858 }
859 return Status::OK;
860}
861
862QStringList FileManager::searchForUnsavedProjects()
863{
864 QDir pencil2DTempDir = QDir::temp();
865 bool folderExists = pencil2DTempDir.cd("Pencil2D");
866 if (!folderExists)
867 {
868 return QStringList();
869 }
870
871 const QStringList nameFilter("*_" PFF_TMP_DECOMPRESS_EXT "_*"); // match name pattern like "Default_Y2xD_0a4e44e9"
872 QStringList entries = pencil2DTempDir.entryList(nameFilter, QDir::Dirs | QDir::Readable);
873
874 QStringList recoverables;
875 for (const QString& path : entries)
876 {
877 QString fullPath = pencil2DTempDir.filePath(path);
878 if (isProjectRecoverable(fullPath))
879 {
880 qDebug() << "Found debris at" << fullPath;
881 recoverables.append(fullPath);
882 }
883 }
884 return recoverables;
885}
886
887bool FileManager::isProjectRecoverable(const QString& projectFolder)
888{
889 QDir dir(projectFolder);
890 if (!dir.exists()) { return false; }
891
892 // There must be a subfolder called "data"
893 if (!dir.exists("data")) { return false; }
894
895 bool ok = dir.cd("data");
896 Q_ASSERT(ok);
897
898 QStringList nameFiler;
899 nameFiler << "*.png" << "*.vec" << "*.xml";
900 QStringList entries = dir.entryList(nameFiler, QDir::Files);
901
902 return (entries.size() > 0);
903}
904
905Object* FileManager::recoverUnsavedProject(QString intermeidatePath)
906{
907 qDebug() << "TODO: recover project" << intermeidatePath;
908
909 QDir projectDir(intermeidatePath);
910 const QString mainXMLPath = projectDir.filePath(PFF_XML_FILE_NAME);
911 const QString dataFolder = projectDir.filePath(PFF_DATA_DIR);
912
913 std::unique_ptr<Object> object(new Object);
914 object->setWorkingDir(intermeidatePath);
915 object->setMainXMLFile(mainXMLPath);
916 object->setDataDir(dataFolder);
917
918 Status st = recoverObject(object.get());
919 if (!st.ok())
920 {
921 mError = st;
922 return nullptr;
923 }
924 // Transfer ownership to the caller
925 return object.release();
926}
927
928Status FileManager::recoverObject(Object* object)
929{
930 // Check whether the main.xml is fine, if not we should make a valid one.
931 bool mainXmlOK = true;
932
933 QFile file(object->mainXMLFile());
934 mainXmlOK &= file.exists();
935 mainXmlOK &= file.open(QFile::ReadOnly);
936 file.close();
937
938 QDomDocument xmlDoc;
939 mainXmlOK &= !!xmlDoc.setContent(&file);
940
941 QDomDocumentType type = xmlDoc.doctype();
942 mainXmlOK &= (type.name() == "PencilDocument" || type.name() == "MyObject");
943
944 QDomElement root = xmlDoc.documentElement();
945 mainXmlOK &= (!root.isNull());
946
947 QDomElement objectTag = root.firstChildElement("object");
948 mainXmlOK &= (!objectTag.isNull());
949
950 if (mainXmlOK == false)
951 {
952 // the main.xml is broken, try to rebuild one
953 rebuildMainXML(object);
954
955 // Load the newly built main.xml
956 QFile file(object->mainXMLFile());
957 file.open(QFile::ReadOnly);
958 xmlDoc.setContent(&file);
959 root = xmlDoc.documentElement();
960 objectTag = root.firstChildElement("object");
961 }
962 loadPalette(object);
963
964 bool ok = loadObject(object, root);
965 verifyObject(object);
966
967 return ok ? Status::OK : Status::FAIL;
968}
969
971Status FileManager::rebuildMainXML(Object* object)
972{
973 QDir dataDir(object->dataDir());
974
975 QStringList nameFiler;
976 nameFiler << "*.png" << "*.vec";
977 const QStringList entries = dataDir.entryList(nameFiler, QDir::Files | QDir::Readable, QDir::Name);
978
979 QMap<int, QStringList> keyFrameGroups;
980
981 // grouping keyframe files by layers
982 for (const QString& s : entries)
983 {
984 int layerIndex = layerIndexFromFilename(s);
985 if (layerIndex > 0)
986 {
987 keyFrameGroups[layerIndex].append(s);
988 }
989 }
990
991 // build the new main XML file
992 const QString mainXMLPath = object->mainXMLFile();
993 QFile file(mainXMLPath);
994 if (!file.open(QFile::WriteOnly | QFile::Text))
995 {
996 return Status::ERROR_FILE_CANNOT_OPEN;
997 }
998
999 QDomDocument xmlDoc("PencilDocument");
1000 QDomElement root = xmlDoc.createElement("document");
1001 QDomProcessingInstruction encoding = xmlDoc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
1002 xmlDoc.appendChild(encoding);
1003 xmlDoc.appendChild(root);
1004
1005 // save editor information
1006 QDomElement projDataXml = saveProjectData(object->data(), xmlDoc);
1007 root.appendChild(projDataXml);
1008
1009 // save object
1010 QDomElement elemObject = xmlDoc.createElement("object");
1011 root.appendChild(elemObject);
1012
1013 for (const int layerIndex : keyFrameGroups.keys())
1014 {
1015 const QStringList& frames = keyFrameGroups.value(layerIndex);
1016 Status st = rebuildLayerXmlTag(xmlDoc, elemObject, layerIndex, frames);
1017 }
1018
1019 QTextStream fout(&file);
1020 xmlDoc.save(fout, 2);
1021 fout.flush();
1022
1023 return Status::OK;
1024}
1033Status FileManager::rebuildLayerXmlTag(QDomDocument& doc,
1034 QDomElement& elemObject,
1035 const int layerIndex,
1036 const QStringList& frames)
1037{
1038 Q_ASSERT(frames.length() > 0);
1039
1040 Layer::LAYER_TYPE type = frames[0].endsWith(".png") ? Layer::BITMAP : Layer::VECTOR;
1041
1042 QDomElement elemLayer = doc.createElement("layer");
1043 elemLayer.setAttribute("id", layerIndex + 1); // starts from 1, not 0.
1044 elemLayer.setAttribute("name", recoverLayerName(type, layerIndex));
1045 elemLayer.setAttribute("visibility", true);
1046 elemLayer.setAttribute("type", type);
1047 elemObject.appendChild(elemLayer);
1048
1049 for (const QString& s : frames)
1050 {
1051 const int framePos = framePosFromFilename(s);
1052 if (framePos < 0) { continue; }
1053
1054 QDomElement elemFrame = doc.createElement("image");
1055 elemFrame.setAttribute("frame", framePos);
1056 elemFrame.setAttribute("src", s);
1057
1058 if (type == Layer::BITMAP)
1059 {
1060 // Since we have no way to know the original img position
1061 // Put it at the top left corner of the default camera
1062 elemFrame.setAttribute("topLeftX", -800);
1063 elemFrame.setAttribute("topLeftY", -600);
1064 }
1065 elemLayer.appendChild(elemFrame);
1066 }
1067 return Status::OK;
1068}
1069
1070QString FileManager::recoverLayerName(Layer::LAYER_TYPE type, int index)
1071{
1072 switch (type)
1073 {
1074 case Layer::BITMAP:
1075 return tr("Bitmap Layer %1").arg(index);
1076 case Layer::VECTOR:
1077 return tr("Vector Layer %1").arg(index);
1078 case Layer::SOUND:
1079 return tr("Sound Layer %1").arg(index);
1080 default:
1081 Q_ASSERT(false);
1082 }
1083 return "";
1084}
1085
1086int FileManager::layerIndexFromFilename(const QString& filename)
1087{
1088 const QStringList tokens = filename.split("."); // e.g., 001.019.png or 012.132.vec
1089 if (tokens.length() >= 3) // a correct file name must have 3 tokens
1090 {
1091 return tokens[0].toInt();
1092 }
1093 return -1;
1094}
1095
1096int FileManager::framePosFromFilename(const QString& filename)
1097{
1098 const QStringList tokens = filename.split("."); // e.g., 001.019.png or 012.132.vec
1099 if (tokens.length() >= 3) // a correct file name must have 3 tokens
1100 {
1101 return tokens[1].toInt();
1102 }
1103 return -1;
1104}
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:971
FileManager::copyDir
Status copyDir(const QDir src, const QDir dst)
Copy a directory to another directory recursively (depth-first).
Definition: filemanager.cpp:775
FileManager::rebuildLayerXmlTag
Status rebuildLayerXmlTag(QDomDocument &doc, QDomElement &elemObject, const int layerIndex, const QStringList &frames)
Rebuild a layer xml tag.
Definition: filemanager.cpp:1033
LayerCamera
Definition: layercamera.h:30
Layer
Definition: layer.h:33
ObjectData
Definition: objectdata.h:27
Object
Definition: object.h:42
ScopeGuard
Definition: util.h:41
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::absolutePath
QString absolutePath() const const
QDir::cd
bool cd(const QString &dirName)
QDir::entryList
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
QDir::exists
bool exists() const const
QDir::filePath
QString filePath(const QString &fileName) const const
QDir::mkpath
bool mkpath(const QString &dirPath) 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()
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 Thu May 8 2025 04:47:53 for Pencil2D by doxygen 1.9.6 based on revision 4513250b1d5b1a3676ec0e67b06b7a885ceaae39