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