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